            uncolored.remove(key)
        current_color += 1
    return key_to_color


def connected_components(adjacency):
    """Identify the connected components of a graph.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    Returns
    -------
    list[list[hashable]]
        A list of connected components,
        with each component a list of connected nodes.

    Examples
    --------
    >>> adjacency = {0: [1, 2], 1: [0, 2], 2: [0, 1], 3: []}
    >>> connected_components(adjacency)
    [[0, 1, 2], [3]]

    """
    tovisit = set(adjacency)
    components = []
    while tovisit:
        root = tovisit.pop()
        visited = breadth_first_traverse(adjacency, root)
        tovisit -= visited
        components.append(list(visited))
    return components


# src/compas/topology/connectivity.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division


def adjacency_from_edges(edges):
    """Construct an adjacency dictionary from a set of edges.

    Parameters
    ----------
    edges : sequence[[hashable, hashable]]
        A list of node identifier pairs.

    Returns
    -------
    dict[hashable, list[hashable]]
        A dictionary mapping each node in the list of node pairs
        to a list of adjacent/connected nodes.

    Examples
    --------
    >>> edges = [[0, 1], [0, 2], [0, 3], [0, 4]]
    >>> adjacency_from_edges(edges)
    {0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}

    """
    adj = {}
    for i, j in iter(edges):
        adj.setdefault(i, []).append(j)
        adj.setdefault(j, []).append(i)
    return adj


# src/compas/topology/orientation.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.geometry import KDTree

from compas.utilities import pairwise
from compas.geometry import centroid_points
from compas.topology import breadth_first_traverse


def unify_cycles(vertices, faces, root=0):
    """Unify the cycle directions of the given faces such that adjacent faces share opposite halfedges.

    Parameters
    ----------
    vertices : sequence[[float, float, float] | :class:`compas.geometry.Point`]
        A list of vertex coordinates.
    faces : sequence[sequence[int]]
        A list of faces with each face defined by a list of indices into the list of vertices.
    root : int, optional
        The starting face.

    Returns
    -------
    list[list[int]]
        A list of faces with the same orientation as the root face.

    Raises
    ------
    AssertionError
        If not all faces were visited.

    Notes
    -----
    The algorithm works by first building an adjacency dict of the faces, which can be traversed efficiently to unify all face cycles.
    Although this process technically only requires the connectivity information contained in the faces,
    the locations of the vertices can be used to speed up execution for very large collections of faces.

    Examples
    --------
    >>> vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]]
    >>> faces = [[0, 1, 2], [0, 3, 2]]
    >>> unify_cycles(vertices, faces)
    [[0, 1, 2], [2, 3, 0]]

    """
    def unify(node, nbr):
        # find the common edge
        for u, v in pairwise(faces[nbr] + faces[nbr][0:1]):
            if u in faces[node] and v in faces[node]:
                # node and nbr have edge u-v in common
                i = faces[node].index(u)
                j = faces[node].index(v)
                if i == j - 1 or (j == 0 and u == faces[node][-1]):
                    # if the traversal of a neighboring halfedge
                    # is in the same direction
                    # flip the neighbor
                    faces[nbr][:] = faces[nbr][::-1]
                    return
    adj = face_adjacency(vertices, faces)
    visited = breadth_first_traverse(adj, root, unify)
    assert len(list(visited)) == len(faces), 'Not all faces were visited'
    return faces


def face_adjacency(xyz, faces):
    """Construct an adjacency dictionary of the given faces, assuming that the faces have arbitrary orientation.

    Parameters
    ----------
    xyz : sequence[[float, float, float] | :class:`compas.geometry.Point`]
        The coordinates of the face vertices.
    faces : sequence[sequence[int]]
        A list of faces with each face defined by a list of indices into the list of xyz coordinates.

    Returns
    -------
    dict[int, list[int]]
        For every face a list of neighbouring faces.

    Notes
    -----
    If the number of faces is larger than one hundred (100),
    this function uses Rhino's RTree for limiting the search for neighbours
    to the immediate surroundings of any given face.

    Examples
    --------
    >>> vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]]
    >>> faces = [[0, 1, 2], [0, 3, 2]]
    >>> face_adjacency(vertices, faces)
    {0: [1], 1: [0]}

    """
    f = len(faces)
    if f > 100:
        return _face_adjacency(xyz, faces)
    adjacency = {}
    for face, vertices in enumerate(faces):
        nbrs = []
        found = set()
        for u, v in pairwise(vertices + vertices[0:1]):
            for nbr, _ in enumerate(faces):
                if nbr == face:
                    continue
                if nbr in found:
                    continue
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if v == a and u == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if u == a and v == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
        adjacency[face] = nbrs
    return adjacency


def _face_adjacency(xyz, faces, nmax=10, radius=2.0):
    points = [centroid_points([xyz[index] for index in face]) for face in faces]
    tree = KDTree(points)
    closest = [tree.nearest_neighbors(point, nmax) for point in points]
    closest = [[index for _, index, _ in nnbrs] for nnbrs in closest]
    adjacency = {}
    for face, vertices in enumerate(faces):
        nbrs = []
        found = set()
        nnbrs = set(closest[face])
        for u, v in pairwise(vertices + vertices[0:1]):
            for nbr in nnbrs:
                if nbr == face:
                    continue
                if nbr in found:
                    continue
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if v == a and u == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if u == a and v == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
        adjacency[face] = nbrs
    return adjacency


# src/compas/topology/orientation_numpy.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from scipy.spatial import cKDTree

from compas.utilities import pairwise
from compas.geometry import centroid_points
from compas.topology import breadth_first_traverse


def unify_cycles_numpy(vertices, faces, root=0):
    """Unify the cycle directions of the given faces such that adjacent faces share opposite halfedges.

    Parameters
    ----------
    vertices : sequence[[float, float, float] | :class:`compas.geometry.Point`]
        A list of vertex coordinates.
    faces : sequence[sequence[int]]
        A list of faces with each face defined by a list of indices into the list of vertices.
    root : int, optional
        The starting face.

    Returns
    -------
    list[list[int]]
        A list of faces with the same orientation as the root face.

    Raises
    ------
    AssertionError
        If not all faces were visited.

    Notes
    -----
    The algorithm works by first building an adjacency dict of the faces, which can be traversed efficiently to unify all face cycles.
    Although this process technically only requires the connectivity information contained in the faces,
    the locations of the vertices can be used to speed up execution for very large collections of faces.

    Examples
    --------
    >>> vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]]
    >>> faces = [[0, 1, 2], [0, 3, 2]]
    >>> unify_cycles_numpy(vertices, faces)
    [[0, 1, 2], [2, 3, 0]]

    """
    def unify(node, nbr):
        # find the common edge
        for u, v in pairwise(faces[nbr] + faces[nbr][0:1]):
            if u in faces[node] and v in faces[node]:
                # node and nbr have edge u-v in common
                i = faces[node].index(u)
                j = faces[node].index(v)
                if i == j - 1 or (j == 0 and u == faces[node][-1]):
                    # if the traversal of a neighboring halfedge
                    # is in the same direction
                    # flip the neighbor
                    faces[nbr][:] = faces[nbr][::-1]
                    return
    adj = face_adjacency_numpy(vertices, faces)
    visited = breadth_first_traverse(adj, root, unify)
    assert len(list(visited)) == len(faces), 'Not all faces were visited'
    return faces


def face_adjacency_numpy(xyz, faces):
    """Construct an adjacency dictionary of the given faces, assuming that the faces have arbitrary orientation.

    Parameters
    ----------
    xyz : sequence[[float, float, float] | :class:`compas.geometry.Point`]
        The coordinates of the face vertices.
    faces : sequence[sequence[int]]
        A list of faces with each face defined by a list of indices into the list of xyz coordinates.

    Returns
    -------
    dict[int, list[int]]
        For every face a list of neighbouring faces.

    Notes
    -----
    If the number of faces is larger than one hundred (100),
    this function uses Scipy's `cKDTree` for limiting the search for neighbours
    to the immediate surroundings of any given face.

    Examples
    --------
    >>> vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]]
    >>> faces = [[0, 1, 2], [0, 3, 2]]
    >>> face_adjacency_numpy(vertices, faces)
    {0: [1], 1: [0]}

    """
    f = len(faces)
    if f > 100:
        return _face_adjacency(xyz, faces)
    adjacency = {}
    for face, vertices in enumerate(faces):
        nbrs = []
        found = set()
        for u, v in pairwise(vertices + vertices[0:1]):
            for nbr, _ in enumerate(faces):
                if nbr == face:
                    continue
                if nbr in found:
                    continue
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if v == a and u == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if u == a and v == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
        adjacency[face] = nbrs
    return adjacency


def _face_adjacency(xyz, faces, nmax=10, radius=2.0):
    points = [centroid_points([xyz[index] for index in face]) for face in faces]
    k = min(len(faces), nmax)
    tree = cKDTree(points)
    _, closest = tree.query(points, k=k, n_jobs=-1)
    adjacency = {}
    for face, vertices in enumerate(faces):
        nbrs = []
        found = set()
        nnbrs = set(closest[face])
        for u, v in pairwise(vertices + vertices[0:1]):
            for nbr in nnbrs:
                if nbr == face:
                    continue
                if nbr in found:
                    continue
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if v == a and u == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if u == a and v == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
        adjacency[face] = nbrs
    return adjacency


# src/compas/topology/orientation_rhino.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from Rhino.Geometry import RTree
from Rhino.Geometry import Sphere
from Rhino.Geometry import Point3d

from compas.utilities import pairwise
from compas.geometry import centroid_points
from compas.topology import breadth_first_traverse


def unify_cycles_rhino(vertices, faces, root=0):
    """Unify the cycle directions of the given faces such that adjacent faces share opposite halfedges.

    Parameters
    ----------
    vertices : sequence[[float, float, float] | :class:`compas.geometry.Point`]
        A list of vertex coordinates.
    faces : sequence[sequence[int]]
        A list of faces with each face defined by a list of indices into the list of vertices.
    root : int, optional
        The starting face.

    Returns
    -------
    list[list[int]]
        A list of faces with the same orientation as the root face.

    Raises
    ------
    AssertionError
        If not all faces were visited.

    Notes
    -----
    The algorithm works by first building an adjacency dict of the faces, which can be traversed efficiently to unify all face cycles.
    Although this process technically only requires the connectivity information contained in the faces,
    the locations of the vertices can be used to speed up execution for very large collections of faces.

    Examples
    --------
    >>> vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]]
    >>> faces = [[0, 1, 2], [0, 3, 2]]
    >>> unify_cycles_rhino(vertices, faces)  # doctest: +SKIP
    [[0, 1, 2], [2, 3, 0]]

    """
    def unify(node, nbr):
        # find the common edge
        for u, v in pairwise(faces[nbr] + faces[nbr][0:1]):
            if u in faces[node] and v in faces[node]:
                # node and nbr have edge u-v in common
                i = faces[node].index(u)
                j = faces[node].index(v)
                if i == j - 1 or (j == 0 and u == faces[node][-1]):
                    # if the traversal of a neighboring halfedge
                    # is in the same direction
                    # flip the neighbor
                    faces[nbr][:] = faces[nbr][::-1]
                    return
    adj = face_adjacency_rhino(vertices, faces)
    visited = breadth_first_traverse(adj, root, unify)
    assert len(list(visited)) == len(faces), 'Not all faces were visited'
    return faces


def face_adjacency_rhino(xyz, faces):
    """Construct an adjacency dictionary of the given faces, assuming that the faces have arbitrary orientation.

    Parameters
    ----------
    xyz : sequence[[float, float, float] | :class:`compas.geometry.Point`]
        The coordinates of the face vertices.
    faces : sequence[sequence[int]]
        A list of faces with each face defined by a list of indices into the list of xyz coordinates.

    Returns
    -------
    dict[int, list[int]]
        For every face a list of neighbouring faces.

    Notes
    -----
    If the number of faces is larger than one hundred (100),
    this function uses Rhino's RTree for limiting the search for neighbours
    to the immediate surroundings of any given face.

    Examples
    --------
    >>> vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]]
    >>> faces = [[0, 1, 2], [0, 3, 2]]
    >>> face_adjacency_rhino(vertices, faces)  # doctest: +SKIP
    {0: [1], 1: [0]}

    """
    f = len(faces)
    if f > 100:
        return _face_adjacency(xyz, faces)
    adjacency = {}
    for face, vertices in enumerate(faces):
        nbrs = []
        found = set()
        for u, v in pairwise(vertices + vertices[0:1]):
            for nbr, _ in enumerate(faces):
                if nbr == face:
                    continue
                if nbr in found:
                    continue
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if v == a and u == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if u == a and v == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
        adjacency[face] = nbrs
    return adjacency


def _face_adjacency(xyz, faces, nmax=10, radius=2.0):
    points = [centroid_points([xyz[index] for index in face]) for face in faces]
    tree = RTree()
    for i, point in enumerate(points):
        tree.Insert(Point3d(* point), i)

    def callback(sender, e):
        data = e.Tag
        data.append(e.Id)
    closest = []
    for i, point in enumerate(points):
        sphere = Sphere(Point3d(* point), radius)
        data = []
        tree.Search(sphere, callback, data)
        closest.append(data)
    adjacency = {}
    for face, vertices in enumerate(faces):
        nbrs = []
        found = set()
        nnbrs = set(closest[face])
        for u, v in pairwise(vertices + vertices[0:1]):
            for nbr in nnbrs:
                if nbr == face:
                    continue
                if nbr in found:
                    continue
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if v == a and u == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
                for a, b in pairwise(faces[nbr] + faces[nbr][0:1]):
                    if u == a and v == b:
                        nbrs.append(nbr)
                        found.add(nbr)
                        break
        adjacency[face] = nbrs
    return adjacency


# src/compas/topology/traversal.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

try:
    from queue import PriorityQueue
except ImportError:
    from Queue import PriorityQueue

from collections import deque

from compas.geometry import distance_point_point


# ==============================================================================
# DFS
# ==============================================================================


def depth_first_ordering(adjacency, root):
    """Compute a depth-first ordering of the nodes of a graph, starting from a root node.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    root : hashable
        The node from which to start the depth-first search.

    Returns
    -------
    list[hashable]
        A depth-first ordering of all nodes of the graph.

    Notes
    -----
    Return all nodes of a connected component containing `root` of a network
    represented by an adjacency dictionary.

    This implementation uses a "to visit" stack. The principle of a stack
    is LIFO. In Python, a list is a stack.

    Initially only the root element is on the stack. While there are still
    elements on the stack, the node on top of the stack is "popped off" and if
    this node was not already visited, its neighbors are added to the stack if
    they hadn't already been visited themselves.

    Since the last element on top of the stack is always popped off, the
    algorithm goes deeper and deeper in the datastructure, until it reaches a
    node without (unvisited) neighbors and then backtracks. Once a new node
    with unvisited neighbors is found, there too it will go as deep as possible
    before backtracking again, and so on. Once there are no more nodes on the
    stack, the entire structure has been traversed.

    Note that this returns a depth-first spanning tree of a connected component
    of the network.

    """
    adjacency = {key: set(nbrs) for key, nbrs in iter(adjacency.items())}
    tovisit = [root]
    visited = set()
    ordering = []

    while tovisit:
        # pop the last added element from the stack
        node = tovisit.pop()
        if node not in visited:
            # mark the node as visited
            visited.add(node)
            ordering.append(node)
            # add the unvisited nbrs to the stack
            tovisit.extend(adjacency[node] - visited)
    return ordering


# def depth_first_tree(adjacency, root):
#     """Construct a spanning tree using a depth-first search.

#     Parameters
#     ----------
#     adjacency : dict
#         An adjacency dictionary.
#     root : hashable
#         The identifier of the root node.

#     Returns
#     -------
#     list
#         List of nodes in depth-first order.
#     dict
#         Dictionary of predecessors for each of the nodes.
#     list
#         The depth-first paths.

#     Examples
#     --------
#     >>>
#     """
#     adjacency = {key: set(nbrs) for key, nbrs in iter(adjacency.items())}
#     tovisit = [root]
#     visited = set()
#     ordering = []
#     predecessors = {}
#     paths = [[root]]

#     while tovisit:
#         # pop the last added element from the stack
#         node = tovisit.pop()
#         if node not in visited:
#             paths[-1].append(node)
#             # mark the node as visited
#             visited.add(node)
#             ordering.append(node)
#             # add the unvisited nbrs to the stack
#             nodes = adjacency[node] - visited
#             if nodes:
#                 for child in nodes:
#                     predecessors[child] = node
#             else:
#                 paths.append([])
#             tovisit.extend(nodes)
#     if not len(paths[-1]):
#         del paths[-1]
#     return ordering, predecessors, paths


# ==============================================================================
# BFS
# ==============================================================================


def breadth_first_ordering(adjacency, root):
    """Compute a breadth-first ordering of the nodes of a graph, starting from a root node.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    root : hashable
        The node from which to start the breadth-first search.

    Returns
    -------
    list[hashable]
        A breadth-first ordering of all nodes of the graph.

    Notes
    -----
    This implementation uses a double-ended queue (deque) to keep track of nodes to visit.
    The principle of a queue is FIFO. In Python, a deque is ideal for removing elements
    from the beginning, i.e. from the 'left'.

    In a breadth-first search, all unvisited neighbors of a node are visited
    first. When a neighbor is visited, its univisited neighbors are added to
    the list of nodes to visit.

    By appending the neighbors to the end of the list of nodes to visit,
    and by visiting the nodes at the start of the list first, the network is
    traversed in *breadth-first* order.

    """
    tovisit = deque([root])
    visited = set([root])
    ordering = [root]
    while tovisit:
        node = tovisit.popleft()
        for nbr in adjacency[node]:
            if nbr not in visited:
                tovisit.append(nbr)
                visited.add(nbr)
                ordering.append(nbr)
    return ordering


def breadth_first_traverse(adjacency, root, callback=None):
    """Traverse an adjacency dict in "breadth-first" order.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    root : hashable
        The identifier of the starting node.
    callback : callable, optional
        A callback function applied to every traversed node and its current neighbour.

    Returns
    -------
    set[hashable]
        The visited nodes.

    """
    tovisit = deque([root])
    visited = set([root])
    while tovisit:
        node = tovisit.popleft()
        for nbr in adjacency[node]:
            if nbr not in visited:
                tovisit.append(nbr)
                visited.add(nbr)
                if callback:
                    callback(node, nbr)
    return visited


def breadth_first_paths(adjacency, root, goal):
    """Return all paths from root to goal.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    root : hashable
        The identifier of the starting node.
    goal : hashable
        The identifier of the ending node.

    Yields
    ------
    list[hashable]
        A path from root to goal.

    Notes
    -----
    Due to the nature of the search, the first path returned is the shortest.

    """
    adjacency = {key: set(nbrs) for key, nbrs in iter(adjacency.items())}
    tovisit = deque([(root, [root])])
    while tovisit:
        node, path = tovisit.popleft()
        for nbr in adjacency[node] - set(path):
            if nbr == goal:
                yield path + [nbr]
            else:
                tovisit.append((nbr, path + [nbr]))


def breadth_first_tree(adjacency, root):
    """Compute a BFS tree, starting from a root node.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    root : hashable
        Identifier of the root node.

    Returns
    -------
    list[hashable]
        BFS ordering of all nodes.
    dict[hashable, hashable]
        A dict mapping each node to its direct predecessor in the tree.
    list[list[hashable]]
        A traversal path for every node in the graph.

    """
    tovisit = deque([root])
    visited = set([root])
    ordering = [root]
    predecessors = {}
    paths = []
    while tovisit:
        node = tovisit.popleft()
        for nbr in adjacency[node]:
            if nbr not in visited:
                predecessors[nbr] = node
                tovisit.append(nbr)
                visited.add(nbr)
                ordering.append(nbr)
            else:
                path = [node]
                while path[-1] in predecessors:
                    path.append(predecessors[path[-1]])
                paths.append(reversed(path))
    return ordering, predecessors, paths


# ==============================================================================
# shortest
# ==============================================================================


def shortest_path(adjacency, root, goal):
    """Find the shortest path between two vertices of a network.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    root : hashable
        The identifier of the starting node.
    goal : hashable
        The identifier of the ending node.

    Returns
    -------
    list[hashable] | None
        The path from root to goal, or None, if no path exists between the vertices.

    """
    try:
        return next(breadth_first_paths(adjacency, root, goal))
    except StopIteration:
        return None


# ==============================================================================
# A*
# ==============================================================================


def reconstruct_path(came_from, current):
    if current not in came_from:
        return None
    total_path = [current]
    while current in came_from:
        current = came_from[current]
        total_path.append(current)
    total_path.reverse()
    return total_path


def astar_lightest_path(adjacency, weights, heuristic, root, goal):
    """Find the path of least weight between two vertices of a graph using the A* search algorithm.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    weights : dict[tuple[hashable, hashable], float]
        A dictionary of edge weights.
    heuristic : dict[hashable, float]
        A dictionary of guesses of weights of paths from a node to the goal.
    root : hashable
        The start vertex.
    goal : hashable
        The end vertex.

    Returns
    -------
    list[hashable] | None
        The path from root to goal, or None, if no path exists between the vertices.

    References
    ----------
    https://en.wikipedia.org/wiki/A*_search_algorithm

    """
    visited_set = set()

    candidates_set = {root}
    best_candidate_heap = PriorityQueue()
    best_candidate_heap.put((heuristic[root], root))

    came_from = dict()

    g_score = dict()
    for v in adjacency:
        g_score[v] = float('inf')
    g_score[root] = 0

    while not best_candidate_heap.empty():
        _, current = best_candidate_heap.get()
        if current == goal:
            break

        visited_set.add(current)
        for neighbor in adjacency[current]:
            if neighbor in visited_set:
                continue

            tentative_g_score = g_score[current] + weights[(current, neighbor)]
            if neighbor not in candidates_set:
                candidates_set.add(neighbor)
            elif tentative_g_score >= g_score[neighbor]:
                continue

            came_from[neighbor] = current
            g_score[neighbor] = tentative_g_score
            new_f_score = g_score[neighbor] + heuristic[neighbor]
            best_candidate_heap.put((new_f_score, neighbor))

    return reconstruct_path(came_from, goal)


def _get_coordinates(key, structure):
    if hasattr(structure, 'node_attributes'):
        return structure.node_attributes(key, 'xyz')
    if hasattr(structure, 'vertex_coordinates'):
        return structure.vertex_coordinates(key)
    raise Exception("Coordinates cannot be found for object of type {}".format(type(structure)))


def _get_points(structure):
    if hasattr(structure, 'nodes'):
        return structure.nodes()
    if hasattr(structure, 'vertices'):
        return structure.vertices()
    raise Exception("Points cannot be found for object of type {}".format(type(structure)))


def astar_shortest_path(graph, root, goal):
    """Find the shortest path between two vertices of a graph or mesh using the A* search algorithm.

    Parameters
    ----------
    graph : :class:`compas.datastructures.Network` | :class:`compas.datastructures.Mesh`
        A network or mesh data structure.
    root : hashable
        The identifier of the starting node.
    goal : hashable
        The identifier of the ending node.

    Returns
    -------
    list[hashable] | None
        The path from root to goal, or None, if no path exists between the vertices.

    References
    ----------
    https://en.wikipedia.org/wiki/A*_search_algorithm

    """
    adjacency = graph.adjacency
    weights = {}
    for u, v in graph.edges():
        u_coords = _get_coordinates(u, graph)
        v_coords = _get_coordinates(v, graph)
        distance = distance_point_point(u_coords, v_coords)
        weights[(u, v)] = distance
        weights[(v, u)] = distance

    heuristic = {}
    goal_coords = _get_coordinates(goal, graph)
    points = _get_points(graph)
    for u in points:
        u_coords = _get_coordinates(u, graph)
        heuristic[u] = distance_point_point(u_coords, goal_coords)

    return astar_lightest_path(adjacency, weights, heuristic, root, goal)


def dijkstra_distances(adjacency, weight, target):
    """Compute Dijkstra distances from all nodes in a graph to one target node.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    weight : dict[tuple[hashable, hashable], float]
        A dictionary of edge weights.
    target : hashable
        The key of the vertex to which the distances are computed.

    Returns
    -------
    dict[hashable, float]
        A dictionary of distances to the target.

    """
    adjacency = {key: set(nbrs) for key, nbrs in adjacency.items()}
    distance = {key: (0 if key == target else 1e+17) for key in adjacency}
    tovisit = set(adjacency.keys())
    visited = set()

    while tovisit:
        u = min(tovisit, key=lambda k: distance[k])
        tovisit.remove(u)
        visited.add(u)
        for v in adjacency[u] - visited:
            d = distance[u] + weight[(u, v)]
            if d < distance[v]:
                distance[v] = d
    return distance


def dijkstra_path(adjacency, weight, source, target, dist=None):
    """Find the shortest path between two nodes of a graph if the weights of the connecting edges are not all the same.

    Parameters
    ----------
    adjacency : dict[hashable, dict[hashable, None]] | dict[hashable, sequence[hashable]]
        An adjacency dictionary representing the connectivity of the graph
        by mapping nodes identifiers to neighbour identifiers.
        Examples of valid adjacency dicts are

        * ``{0: [1, 2, 3, 4], 1: [0], 2: [0], 3: [0], 4: [0]}``
        * ``{0: {1: None, 2: None, 3: None, 4: None}, 1: {0: None}, 2: {0: None}, 3: {0: None}, 4: {0: None}}``

    weight : dict[tuple[hashable, hashable], float]
        A dictionary of edge weights.
    source : hashable
        The start vertex.
    target : hashable
        The end vertex.

    Returns
    -------
    list[hashable]
        The shortest path.

    Notes
    -----
    The edge weights should all be positive.
    For a directed graph, set the weights of the reversed edges to ``+inf``.
    For an undirected graph, add the same weight for an edge in both directions.

    """
    if not dist:
        dist = dijkstra_distances(adjacency, weight, target)
    path = [source]
    node = source
    node = min(adjacency[node], key=lambda nbr: dist[nbr] + weight[(node, nbr)])
    path.append(node)
    while node != target:
        node = min(adjacency[node], key=lambda nbr: dist[nbr] + weight[(node, nbr)])
        path.append(node)
    return path


# src/compas/utilities/__init__.py

"""
********************************************************************************
utilities
********************************************************************************

.. currentmodule:: compas.utilities

.. rst-class:: lead

This package collects miscellaneous functionality with the common characteristic
that it simply did not really fit anywhere else.


async
=====

.. autosummary::
    :toctree: generated/
    :nosignatures:

    await_callback


colors
======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    i_to_rgb
    i_to_red
    i_to_green
    i_to_blue
    i_to_white
    i_to_black
    rgb_to_hex
    color_to_colordict
    color_to_rgb


encoders
========

.. autosummary::
    :toctree: generated/
    :nosignatures:

    DataEncoder
    DataDecoder


itertools
=========

.. autosummary::
    :toctree: generated/
    :nosignatures:

    flatten
    linspace
    meshgrid
    pairwise
    window
    iterable_like
    normalize_values
    remap_values


maps
====

.. autosummary::
    :toctree: generated/
    :nosignatures:

    geometric_key
    reverse_geometric_key
    geometric_key_xy


"""
from __future__ import absolute_import

from .azync import await_callback
from .coercing import (
    coerce_sequence_of_list,
    coerce_sequence_of_tuple,
    is_item_iterable,
    is_sequence_of_dict,
    is_sequence_of_float,
    is_sequence_of_int,
    is_sequence_of_iterable,
    is_sequence_of_list,
    is_sequence_of_str,
    is_sequence_of_tuple
)
from .colors import (
    Colormap,
    black,
    blue,
    color_to_colordict,
    color_to_rgb,
    cyan,
    green,
    hex_to_rgb,
    i_to_black,
    i_to_blue,
    i_to_green,
    i_to_red,
    i_to_rgb,
    i_to_white,
    is_color_hex,
    is_color_light,
    is_color_rgb,
    red,
    rgb_to_hex,
    rgb_to_rgb,
    white,
    yellow
)
from .datetime import (
    now,
    timestamp
)
from .decorators import (
    abstractclassmethod,
    abstractstaticmethod,
    memoize,
    print_profile,
)
from .descriptors import (
    Float,
    RGBColour
)
from .encoders import (
    DataDecoder,
    DataEncoder
)
from .images import gif_from_images
from .itertools import (
    flatten,
    grouper,
    iterable_like,
    linspace,
    meshgrid,
    normalize_values,
    pairwise,
    remap_values,
    window
)
from .maps import (
    geometric_key,
    geometric_key_xy,
    reverse_geometric_key
)
from .remote import download_file_from_remote
from .ssh import SSH
from .xfunc import XFunc

__all__ = [
    'await_callback',
    'is_sequence_of_str',
    'is_sequence_of_int',
    'is_sequence_of_float',
    'is_sequence_of_tuple',
    'is_sequence_of_list',
    'is_sequence_of_dict',
    'is_sequence_of_iterable',
    'is_item_iterable',
    'coerce_sequence_of_tuple',
    'coerce_sequence_of_list',
    'i_to_rgb',
    'i_to_red',
    'i_to_green',
    'i_to_blue',
    'i_to_white',
    'i_to_black',
    'is_color_rgb',
    'is_color_hex',
    'is_color_light',
    'rgb_to_hex',
    'rgb_to_rgb',
    'hex_to_rgb',
    'color_to_colordict',
    'color_to_rgb',
    'Colormap',
    'red',
    'green',
    'blue',
    'yellow',
    'cyan',
    'white',
    'black',
    'timestamp',
    'now',
    'abstractstaticmethod',
    'abstractclassmethod',
    'memoize',
    'print_profile',
    'Float',
    'RGBColour',
    'DataDecoder',
    'DataEncoder',
    'gif_from_images',
    'normalize_values',
    'remap_values',
    'meshgrid',
    'linspace',
    'flatten',
    'pairwise',
    'window',
    'iterable_like',
    'grouper',
    'geometric_key',
    'reverse_geometric_key',
    'geometric_key_xy',
    'download_file_from_remote',
    'SSH',
    'XFunc'
]


# src/compas/utilities/azync.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import sys
import threading


__all__ = ['await_callback']


class ThreadExceptHookHandler(object):
    """Workaround to deal with a bug in the Python interpreter (!).

    Report: http://bugs.python.org/issue1230540
    Discussion: https://stackoverflow.com/a/31622038/269335
    PR (not yet merged): https://github.com/python/cpython/pull/8610
    Disclaimer (!): https://news.ycombinator.com/item?id=11090814

    """

    def __enter__(self):
        original_init = threading.Thread.__init__

        def init(self, *args, **kwargs):
            original_init(self, *args, **kwargs)
            original_run = self.run

            def run_with_except_hook(*args2, **kwargs2):
                try:
                    original_run(*args2, **kwargs2)
                except Exception:
                    sys.excepthook(*sys.exc_info())

            self.run = run_with_except_hook

        self._original_init = original_init
        threading.Thread.__init__ = init

        return self

    def __exit__(self, *args):
        threading.Thread.__init__ = self._original_init


def await_callback(async_func, callback_name='callback', errback_name=None, *args, **kwargs):
    """Wait for the completion of an asynchronous code that uses callbacks to signal completion.

    This helper function turns an async function into a synchronous one,
    waiting for its completion before moving forward (without doing a busy wait).

    It is useful to minimize "callback hell" when more advanced options
    like `asyncio` are not available.

    Parameters
    ----------
    async_func : callable
        An asynchronous function that receives at least one callback parameter
        to signal completion.
    callback_name : string, optional
        Name of the callback parameter of `async_func`.
    errback_name : string, optional
        Name of the error handling callback parameter of `async_func`.

    Returns
    -------
    ???

    Notes
    -----
    Exceptions thrown during the async execution are handled and re-thrown as normal
    exceptions, even if they were raised on a different thread.

    Examples
    --------
    The following example shows how to await an async function (``do_async_stuff`` in
    the example), using this utility:

    .. code-block:: python

        from compas.utilities import await_callback

        def do_async_stuff(callback):
            from threading import Thread

            def runner(cb):
                print('doing async stuff')
                # ..
                cb('done')

            Thread(target=runner, args=(callback, )).start()

        result = await_callback(do_async_stuff)

    """
    wait_event = threading.Event()
    call_results = {}

    def inner_callback(*args, **kwargs):
        try:
            call_results['args'] = args
            call_results['kwargs'] = kwargs
            wait_event.set()
        except Exception as e:
            call_results['exception'] = e
            wait_event.set()

    kwargs['callback'] = inner_callback
    if errback_name:
        def inner_errback(error):
            if isinstance(error, Exception):
                call_results['exception'] = error
            else:
                call_results['exception'] = Exception(str(error))
            wait_event.set()

        kwargs[errback_name] = inner_errback

    def unhandled_exception_handler(type, value, traceback):
        call_results['exception'] = value
        wait_event.set()

    try:
        # Install unhanlded exception handler
        sys.excepthook = unhandled_exception_handler

        # Invoke async method and wait
        with ThreadExceptHookHandler():
            async_func(*args, **kwargs)
            wait_event.wait()
    finally:
        # Restore built-in unhanled exception handler
        sys.excepthook = sys.__excepthook__

    if 'exception' in call_results:
        raise call_results['exception']

    return_value = call_results['args']
    dict_values = call_results['kwargs']

    if not dict_values:
        # If nothing, then None
        if len(return_value) == 0:
            return None
        # If it's a one-item tuple,
        # un-wrap from it and return that element
        elif len(return_value) == 1:
            return return_value[0]
        else:
            return return_value

    if not return_value:
        return dict_values

    return return_value + (dict_values,)


# src/compas/utilities/coercing.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import warnings

from compas.data.coercion import coerce_sequence_of_list
from compas.data.coercion import coerce_sequence_of_tuple
from compas.data.validators import is_sequence_of_str
from compas.data.validators import is_sequence_of_int
from compas.data.validators import is_sequence_of_float
from compas.data.validators import is_sequence_of_tuple
from compas.data.validators import is_sequence_of_list
from compas.data.validators import is_sequence_of_dict
from compas.data.validators import is_sequence_of_iterable
from compas.data.validators import is_item_iterable


__all__ = [
    'is_sequence_of_str',
    'is_sequence_of_int',
    'is_sequence_of_float',
    'is_sequence_of_tuple',
    'is_sequence_of_list',
    'is_sequence_of_dict',
    'is_sequence_of_iterable',

    'is_item_iterable',

    'coerce_sequence_of_tuple',
    'coerce_sequence_of_list',
]

warnings.warn(
    "The coercing module in utilities is deprecated. Use the data module instead",
    DeprecationWarning,
    stacklevel=2
)


# src/compas/utilities/colors.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import warnings

import re

try:
    basestring
except NameError:
    basestring = str

warnings.warn(
    "The colors module in utilities is deprecated. Use compas.colors instead",
    DeprecationWarning,
    stacklevel=2
)

red = 255, 0, 0
orange = 255, 125, 0
yellow = 255, 255, 0
yellowish = 125, 255, 0
green = 0, 255, 0
greenish = 0, 255, 125
cyan = 0, 255, 255
cyanish = 0, 125, 255
blue = 0, 0, 255

white = 255, 255, 255
black = 0, 0, 0


BASE16 = '0123456789abcdef'

try:
    HEX_DEC = {v: int(v, base=16) for v in [x + y for x in BASE16 for y in BASE16]}
except Exception:
    HEX_DEC = {v: int(v, 16) for v in [x + y for x in BASE16 for y in BASE16]}


def i_to_rgb(i, normalize=False):
    """Convert a number between 0.0 and 1.0 to an equivalent RGB tuple.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    i : float
        A number between 0.0 and 1.0.
    normalize : bool, optional
        If True, normalize the resulting RGB values.
        Default is to return integer values ranging from 0 to 255.

    Returns
    -------
    tuple
        The RGB values of the color corresponding to the provided number.
        If `normalize` is true, the RGB values are normalized to values between 0.0 and 1.0.
        If `normalize` is false, the RGB values are integers between 0 and 255.

    Examples
    --------
    >>> i_to_rgb(1.0)
    (255, 0, 0)
    >>> i_to_rgb(0.75)
    (255, 255, 0)
    >>> i_to_rgb(0.5)
    (0, 255, 0)
    >>> i_to_rgb(0.25)
    (0, 255, 255)
    >>> i_to_rgb(0.0)
    (0, 0, 255)

    >>> i_to_rgb(1.0, True)
    (1.0, 0.0, 0.0)
    >>> i_to_rgb(0.75, True)
    (1.0, 1.0, 0.0)
    >>> i_to_rgb(0.5, True)
    (0.0, 1.0, 0.0)
    >>> i_to_rgb(0.25, True)
    (0.0, 1.0, 1.0)
    >>> i_to_rgb(0.0, True)
    (0.0, 0.0, 1.0)

    """
    i = max(i, 0.0)
    i = min(i, 1.0)
    if i == 0.0:
        r, g, b = 0, 0, 255
    elif 0.0 < i < 0.25:
        r, g, b = 0, int(255 * (4 * i)), 255
    elif i == 0.25:
        r, g, b = 0, 255, 255
    elif 0.25 < i < 0.5:
        r, g, b = 0, 255, int(255 - 255 * 4 * (i - 0.25))
    elif i == 0.5:
        r, g, b = 0, 255, 0
    elif 0.5 < i < 0.75:
        r, g, b = int(0 + 255 * 4 * (i - 0.5)), 255, 0
    elif i == 0.75:
        r, g, b = 255, 255, 0
    elif 0.75 < i < 1.0:
        r, g, b,  = 255, int(255 - 255 * 4 * (i - 0.75)), 0
    elif i == 1.0:
        r, g, b = 255, 0, 0
    else:
        r, g, b = 0, 0, 0
    if not normalize:
        return r, g, b
    return r / 255.0, g / 255.0, b / 255.0


def i_to_red(i, normalize=False):
    """Convert a number between 0.0 and 1.0 to a shade of red.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    i : float
        A number between 0.0 and 1.0.
    normalize : bool, optional
        If True, normalize the resulting RGB values.
        Default is to return integer values ranging from 0 to 255.

    Returns
    -------
    tuple
        The RGB values of the color corresponding to the provided number.
        If `normalize` is true, the RGB values are normalized to values between 0.0 and 1.0.
        If `normalize` is false, the RGB values are integers between 0 and 255.

    Examples
    --------
    >>> i_to_red(1.0)
    (255, 0, 0)
    >>> i_to_red(0.0)
    (255, 255, 255)

    """
    i = max(i, 0.0)
    i = min(i, 1.0)
    g = b = min((1 - i) * 255, 255)
    if not normalize:
        return 255, int(g), int(b)
    return 1.0, g / 255, b / 255


def i_to_green(i, normalize=False):
    """Convert a number between 0.0 and 1.0 to a shade of green.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    i : float
        A number between 0.0 and 1.0.
    normalize : bool, optional
        If True, normalize the resulting RGB values.
        Default is to return integer values ranging from 0 to 255.

    Returns
    -------
    tuple
        The RGB values of the color corresponding to the provided number.
        If `normalize` is true, the RGB values are normalized to values between 0.0 and 1.0.
        If `normalize` is false, the RGB values are integers between 0 and 255.

    Examples
    --------
    >>> i_to_green(1.0)
    (0, 255, 0)
    >>> i_to_green(0.0)
    (255, 255, 255)

    """
    i = max(i, 0.0)
    i = min(i, 1.0)
    r = b = min((1 - i) * 255, 255)
    if not normalize:
        return int(r), 255, int(b)
    return r / 255, 1.0, b / 255


def i_to_blue(i, normalize=False):
    """Convert a number between 0.0 and 1.0 to a shade of blue.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    i : float
        A number between 0.0 and 1.0.
    normalize : bool, optional
        If True, normalize the resulting RGB values.
        Default is to return integer values ranging from 0 to 255.

    Returns
    -------
    tuple
        The RGB values of the color corresponding to the provided number.
        If `normalize` is true, the RGB values are normalized to values between 0.0 and 1.0.
        If `normalize` is false, the RGB values are integers between 0 and 255.

    Examples
    --------
    >>> i_to_blue(1.0)
    (0, 0, 255)
    >>> i_to_blue(0.0)
    (255, 255, 255)

    """
    i = max(i, 0.0)
    i = min(i, 1.0)
    r = g = min((1 - i) * 255, 255)
    if not normalize:
        return int(r), int(g), 255
    return r / 255, g / 255, 1.0


def i_to_white(i, normalize=False):
    """Convert a number between 0.0 and 1.0 to a shade of white.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    i : float
        A number between 0.0 and 1.0.
    normalize : bool, optional
        If True, normalize the resulting RGB values.
        Default is to return integer values ranging from 0 to 255.

    Returns
    -------
    tuple
        The RGB values of the color corresponding to the provided number.
        If `normalize` is true, the RGB values are normalized to values between 0.0 and 1.0.
        If `normalize` is false, the RGB values are integers between 0 and 255.

    Examples
    --------
    >>> i_to_white(1.0)
    (255, 255, 255)
    >>> i_to_white(0.0)
    (0, 0, 0)

    """
    i = max(i, 0.0)
    i = min(i, 1.0)
    rgb = min(i * 255, 255)
    if not normalize:
        return int(rgb), int(rgb), int(rgb)
    rgb = rgb / 255
    return rgb, rgb, rgb


def i_to_black(i, normalize=False):
    """Convert a number between 0.0 and 1.0 to a shade of black.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    i : float
        A number between 0.0 and 1.0.
    normalize : bool, optional
        If True, normalize the resulting RGB values.
        Default is to return integer values ranging from 0 to 255.

    Returns
    -------
    tuple
        The RGB values of the color corresponding to the provided number.
        If `normalize` is true, the RGB values are normalized to values between 0.0 and 1.0.
        If `normalize` is false, the RGB values are integers between 0 and 255.

    Examples
    --------
    >>> i_to_black(1.0)
    (0, 0, 0)
    >>> i_to_black(0.0)
    (255, 255, 255)

    """
    i = max(i, 0.0)
    i = min(i, 1.0)
    rgb = min((1 - i) * 255, 255)
    if not normalize:
        return int(rgb), int(rgb), int(rgb)
    rgb = rgb / 255
    return rgb, rgb, rgb


class Colormap(object):
    """Convenience class for converting a data range into a corresponding RGB color range.

    .. deprecated:: 1.14
        Use :class:`compas.colors.ColorMap` instead.

    Parameters
    ----------
    data : list
        A list of data points.
    spec : {'rgb', 'red', 'green', 'blue', 'white', 'black'}
        A color specification.

    Class Attributes
    ----------------
    colorfuncs : dict[str, callable]
        A dictionary mapping color specification names to corresponding converters.

    Examples
    --------
    >>> data = list(range(10))
    >>> cmap = Colormap(data, 'rgb')
    >>> for d in data:
    ...     cmap(d)
    ...
    (0, 0, 255)
    (0, 113, 255)
    (0, 226, 255)
    (0, 255, 170)
    (0, 255, 56)
    (56, 255, 0)
    (169, 255, 0)
    (255, 226, 0)
    (255, 113, 0)
    (255, 0, 0)

    """

    colorfuncs = {
        'rgb': i_to_rgb,
        'red': i_to_red,
        'green': i_to_green,
        'blue': i_to_blue,
        'white': i_to_white,
        'black': i_to_black
    }

    def __init__(self, data, spec):
        self.data = data
        self.dmin = min(data)
        self.dmax = max(data)
        self.dspan = self.dmax - self.dmin
        self.colorfunc = Colormap.colorfuncs[spec]

    def __call__(self, value):
        i = (value - self.dmin) / (self.dspan)
        return self.colorfunc(i)


def is_color_rgb(color):
    """Is a color in a valid RGB format.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    color : [int, int, int] | [float, float, float]
        The color object.

    Returns
    -------
    bool
        True, if the color object is in RGB format.
        False, otherwise.

    Examples
    --------
    >>> color = (255, 0, 0)
    >>> is_color_rgb(color)
    True
    >>> color = (1.0, 0.0, 0.0)
    >>> is_color_rgb(color)
    True
    >>> color = (1.0, 0, 0)
    >>> is_color_rgb(color)
    False
    >>> color = (255, 0.0, 0.0)
    >>> is_color_rgb(color)
    False
    >>> color = (256, 0, 0)
    >>> is_color_rgb(color)
    False

    """
    if isinstance(color, (tuple, list)):
        if len(color) == 3:
            if all(isinstance(c, float) for c in color):
                if all(c >= 0.0 and c <= 1.0 for c in color):
                    return True
            elif all(isinstance(c, int) for c in color):
                if all(c >= 0 and c <= 255 for c in color):
                    return True
    return False


def is_color_hex(color):
    """Is a color in a valid HEX format.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    color : str
        The color object.

    Returns
    -------
    bool
        True, if the color object is in HEX format.
        False, otherwise.

    Examples
    --------
    >>> is_color_hex("#ff0000")
    True
    >>> is_color_hex("#f00")
    True
    >>> is_color_hex("#f000")
    False
    >>> is_color_hex("#ff000")
    False

    """
    if isinstance(color, basestring):
        match = re.search(r'^#(?:[0-9a-fA-F]{3}){1,2}$', color)
        if match:
            return True
        return False
    return False


def rgb_to_rgb(rgb, g=None, b=None):
    """Convert an RGB color specification to an integer-based RGB color specification.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    rgb : int or float | [int, int, int] | [float, float, float]
        A full RGB color specification, or an integer or a float representing the value of the red component.
    g : int | float, optional
        The green component.
        This parameter is ignored if `rgb` is a full color specification.
    b : int | float, optional
        The blue component.
        This parameter is ignored if `rgb` is a full color specification.

    Returns
    -------
    tuple[int, int, int]
        Three RGB color components in integer format, each in the range of 0-255.

    Examples
    --------
    >>> rgb_to_rgb((255, 0.0, 0.0))
    (255, 0, 0)
    >>> rgb_to_rgb((255, 0, 0))
    (255, 0, 0)
    >>> rgb_to_rgb((1.0, 0, 0))
    (255, 0, 0)
    >>> rgb_to_rgb((1, 0, 0))
    (1, 0, 0)

    """
    if g is None and b is None:
        r, g, b = rgb
    else:
        r = rgb
    r = max(0, min(r, 255))
    g = max(0, min(g, 255))
    b = max(0, min(b, 255))
    if any(isinstance(c, float) for c in (r, g, b)) and all(c <= 1.0 for c in (r, g, b)):
        r = r * 255.0
        g = g * 255.0
        b = b * 255.0
    if all(c > 0.0 and c < 1.0 for c in (r, g, b)):
        r = r * 255.0
        g = g * 255.0
        b = b * 255.0
    return int(r), int(g), int(b)


def rgb_to_hex(rgb, g=None, b=None):
    """Convert an RGB color specification to HEX.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    rgb : int or float | [int, int, int] | [float, float, float]
        A full RGB color specification, or an integer or a float representing the value of the red component.
    g : int | float, optional
        The green component.
        This parameter is ignored if `rgb` is a full color specification.
    b : int | float, optional
        The blue component.
        This parameter is ignored if `rgb` is a full color specification.

    Returns
    -------
    str
        The corresponding HEX color.

    Examples
    --------
    >>> rgb_to_hex((255, 0.0, 0.0))
    '#ff0000'
    >>> rgb_to_hex((255, 0, 0))
    '#ff0000'
    >>> rgb_to_hex((1.0, 0, 0))
    '#ff0000'
    >>> rgb_to_hex((1, 0, 0))
    '#010000'

    """
    r, g, b = rgb_to_rgb(rgb, g=g, b=b)
    return '#{0:02x}{1:02x}{2:02x}'.format(r, g, b)


def hex_to_rgb(value, normalize=False):
    """Convert a HEX color to the corresponding RGB format.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    value : str
        A HEX color.
    normalize : bool, optional
        Normalize the RGB components if true.

    Returns
    -------
    tuple[int, int, int] | tuple[float, float, float]
        If `normalize` is True, the RGB color with 0-1 float components.
        If `normalize` is False, the RGB color with 0-255 integer components.

    Examples
    --------
    >>> hex_to_rgb('#ff0000')
    (255, 0, 0)
    >>> hex_to_rgb('#ff0000', normalize=True)
    (1.0, 0.0, 0.0)

    """
    value = value.lstrip('#').lower()
    r = HEX_DEC[value[0:2]]
    g = HEX_DEC[value[2:4]]
    b = HEX_DEC[value[4:6]]
    if normalize:
        return r / 255.0, g / 255.0, b / 255.0
    return r, g, b


def color_to_rgb(color, normalize=False):
    """Convert a HEX or RGB color to RGB.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    color : str | [int, int, int] | [float, float, float]
        The color.
    normalize : bool, optional
        If True, normalize the resulting RGB color components.

    Returns
    -------
    tuple[int, int, int] | tuple[float, float, float]
        If `normalize` is True, the RGB color with 0-1 float components.
        If `normalize` is False, the RGB color with 0-255 integer components.

    Examples
    --------
    >>> color_to_rgb('#ff0000')
    (255, 0, 0)
    >>> color_to_rgb('#ff0000', normalize=True)
    (1.0, 0.0, 0.0)
    >>> color_to_rgb(1.0, normalize=True)
    (1.0, 0.0, 0.0)
    >>> color_to_rgb(1.0)
    (255, 0, 0)
    >>> color_to_rgb((255, 0, 0))
    (255, 0, 0)
    >>> color_to_rgb((255, 0, 0), normalize=True)
    (1.0, 0.0, 0.0)

    """
    if isinstance(color, basestring):
        r, g, b = hex_to_rgb(color)
    elif isinstance(color, float):
        r, g, b = i_to_rgb(color)
    else:
        r, g, b = color
    if not normalize:
        return r, g, b
    if isinstance(r, float):
        return r, g, b
    return r / 255., g / 255., b / 255.


def color_to_colordict(color, keys, default=None, colorformat='rgb', normalize=False):
    """Convert a color specification to a dict of colors.

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    color : str or [int, int, int] | [float, float, float] | dict[hashable, [int, int, int]] | dict[hashable, [float, float, float]]
        The base color specification.
        This can be a single color (as HEX or RGB), a list of colors, or a dict of colors.
    keys : sequence[hashable]
        The keys of the color dict.
    default : str | tuple[int, int, int] | tuple[float, float, float], optional
        A valid color specification (HEX or RGB).
    colorformat : Literal['hex', 'rgb'], optional
        The format of the colors in the color dict.
    normalize : bool, optional
        If True and `colorformat` is ``'rgb'``, normalize the color components.

    Returns
    -------
    dict[hashable, tuple[int, int, int]] | dict[hashable, tuple[float, float, float]]
        A dictionary mapping the provided keys to the provided color(s).

    Raises
    ------
    Exception
        If the value of `color`, or the value of `colorformat` is not valid.

    Examples
    --------
    >>> color_to_colordict('#ff0000', [0, 1, 2])
    {0: (255, 0, 0), 1: (255, 0, 0), 2: (255, 0, 0)}
    >>> color_to_colordict('#ff0000', [0, 1, 2], colorformat='hex')
    {0: '#ff0000', 1: '#ff0000', 2: '#ff0000'}
    >>> color_to_colordict('#ff0000', [0, 1, 2], colorformat='rgb', normalize=True)
    {0: (1.0, 0.0, 0.0), 1: (1.0, 0.0, 0.0), 2: (1.0, 0.0, 0.0)}

    """
    color = color or default
    if color is None:
        return {key: None for key in keys}
    # if input is hex
    if isinstance(color, basestring):
        # and output should be rgb
        if colorformat == 'rgb':
            color = hex_to_rgb(color, normalize=normalize)
        return {key: color for key in keys}
    # if input is rgb
    if isinstance(color, (tuple, list)) and len(color) == 3:
        # and output should be hex
        if colorformat == 'hex':
            color = rgb_to_hex(color, normalize=normalize)
        # and output should be rgb
        # else:
        #     color = rgb_to_rgb(color, normalize=normalize)
        return {key: color for key in keys}
    if isinstance(color, dict):
        for k, c in color.items():
            # if input is hex
            if isinstance(c, basestring):
                # and output should be rgb
                if colorformat == 'rgb':
                    color[k] = hex_to_rgb(c, normalize=normalize)
            # if input is rgb
            elif isinstance(c, (tuple, list)) and len(c) == 3:
                # and output should be hex
                if colorformat == 'hex':
                    color[k] = rgb_to_hex(c, normalize=normalize)
                # and output should be rgb
                # else:
                #     color[k] = rgb_to_rgb(c, normalize=normalize)
        return {key: (default if key not in color else color[key]) for key in keys}
    raise Exception('This is not a valid color format: {0}'.format(type(color)))


def is_color_light(color):
    r"""Is a color "light".

    .. deprecated:: 1.14
        Use :class:`compas.colors.Color` instead.

    Parameters
    ----------
    color: str | tuple[float, float, float] | tuple[int, int, int]
        The color specification in HEX or RGB format.

    Returns
    -------
    bool
        True, if the color is light.
        False, otherwise.

    Notes
    -----
    A color is considered "light" if the following is True for normalized RGB components:

    .. math::

        0.2126   \frac{r + 0.055}{1.055}^2.4
        + 0.7152 \frac{g + 0.055}{1.055}^2.4
        + 0.0722 \frac{b + 0.055}{1.055}^2.4 > 0.179

    """
    if is_color_hex(color):
        rgb = hex_to_rgb(color)
    else:
        rgb = color
    r, g, b = rgb_to_rgb(rgb)
    r = r / 255.0
    g = g / 255.0
    b = b / 255.0
    r = ((r + 0.055) / 1.055) ** 2.4
    g = ((g + 0.055) / 1.055) ** 2.4
    b = ((b + 0.055) / 1.055) ** 2.4
    L = 0.2126 * r + 0.7152 * g + 0.0722 * b
    return L > 0.179


# src/compas/utilities/datetime.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import time
import datetime


__all__ = [
    'timestamp',
    'now'
]


def timestamp():
    """Generate a timestamp using the current date and time.

    Returns
    -------
    str
        The timestamp.

    Examples
    --------
    >>> type(timestamp()) == type('')
    True

    """
    return datetime.datetime.fromtimestamp(time.time()).strftime('%Y%m%d%H%M%S')


def now():
    """Generate a timestamp using the current date and time.

    Returns
    -------
    str
        The timestamp.

    Examples
    --------
    >>> type(now()) == type('')
    True

    """
    return timestamp()


# src/compas/utilities/decorators.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import functools
import pstats

from functools import wraps

try:
    from cStringIO import StringIO
except ImportError:
    try:
        from StringIO import StringIO
    except ImportError:
        from io import StringIO

try:
    import cProfile as Profile
except ImportError:
    import profile as Profile


__all__ = [
    'abstractstaticmethod',
    'abstractclassmethod',
    'memoize',
    'print_profile',
]


class abstractstaticmethod(staticmethod):
    """Decorator for declaring a static method abstract.

    Parameters
    ----------
    function : callable
        The method to declare abstract static.

    """

    __slots__ = ()

    __isabstractmethod__ = True

    def __init__(self, function):
        function.__isabstractmethod__ = True
        super(abstractstaticmethod, self).__init__(function)


class abstractclassmethod(classmethod):
    """Decorator for declaring a class method abstract.

    Parameters
    ----------
    function : callable
        The class method to declare abstract.

    """

    __slots__ = ()

    __isabstractmethod__ = True

    def __init__(self, function):
        function.__isabstractmethod__ = True
        super(abstractclassmethod, self).__init__(function)


def memoize(func, *args, **kwargs):
    """Decorator to wrap a function with a memoizing callable.

    Parameters
    ----------
    func : callable
        The function that should be memoized.
    *args : list
        Positional arguments to be passed on to the wrapper function.
    **kwargs : dict, optional
        Optional keyword arguments to be passed on to the wrapper function.

    Returns
    -------
    callable
        A wrapper for the original function that returns a previously
        computed and cached result when possible.

    """
    cache = func.cache = {}

    @functools.wraps(func)
    def memoized_func(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]

    return memoized_func


def print_profile(func):
    """Decorate a function with automatic profile printing.

    Parameters
    ----------
    func : callable
        The function to decorate.

    Returns
    -------
    callable
        The decorated function.

    Examples
    --------
    .. code-block:: python

        @print_profile
        def f(n):
            return sum(for i in range(n))

        print(f(100))
        print(f.__doc__)
        print(f.__name__)

    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        profile = Profile.Profile()
        profile.enable()
        #
        res = func(*args, **kwargs)
        #
        profile.disable()
        stream = StringIO()
        stats = pstats.Stats(profile, stream=stream)
        stats.strip_dirs()
        stats.sort_stats(1)
        stats.print_stats(20)
        print(stream.getvalue())
        #
        return res
    return wrapper


# src/compas/utilities/descriptors.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import warnings


__all__ = [
    'Float',
    'RGBColour',
]


class Float(object):
    def __init__(self):
        warnings.warn("This class will be removed.",
                      DeprecationWarning)


class RGBColour(object):
    def __init__(self):
        warnings.warn("This class will be removed.",
                      DeprecationWarning)


# src/compas/utilities/encoders.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import warnings

from compas.data import DataEncoder
from compas.data import DataDecoder

__all__ = [
    'DataEncoder',
    'DataDecoder'
]

warnings.warn(
    "The encoders module in utilities is deprecated. Use the encoders module from data instead",
    DeprecationWarning,
    stacklevel=2
)


# src/compas/utilities/images.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import os

try:
    import imageio
except ImportError:
    pass


__all__ = ['gif_from_images']


def gif_from_images(files,
                    gif_path,
                    fps=10,
                    loop=0,
                    reverse=False,
                    pingpong=False,
                    subrectangles=True,
                    delete_files=False):
    """Create an animated GIF from a series of images.

    Parameters
    ----------
    files : list
        The image series.
    gif_path : str
        The location to save the GIF.
    fps : int, optional
        Frames per second.
    loop : int, optional
        The number of loops.
    reverse : bool, optional
        If True, reverse the image series.
    pingpong : bool, optional
        If True, add a reverse sequence to the end of the base sequence to go back to the beginning.
    subrectangles : bool, optional
        If True, optimize the file size by looking for invariant subrectangles.

    Returns
    -------
    None

    """
    if reverse:
        files.reverse()
    if pingpong:
        files += files[::-1]
    with imageio.get_writer(gif_path,
                            mode='I',
                            fps=fps,
                            loop=loop,
                            subrectangles=subrectangles) as writer:
        for filename in files:
            image = imageio.imread(filename)
            writer.append_data(image)
    if delete_files:
        for filename in files:
            os.remove(filename)


# src/compas/utilities/itertools.py

# recipes with itertools
# see: https://docs.python.org/3.6/library/itertools.html
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from itertools import islice
from itertools import chain
from itertools import repeat
from itertools import tee

try:
    from itertools import zip_longest
except ImportError:
    from itertools import izip_longest as zip_longest


__all__ = [
    'normalize_values',
    'remap_values',
    'meshgrid',
    'linspace',
    'flatten',
    'pairwise',
    'window',
    'iterable_like',
    'grouper'
]


def normalize_values(values, new_min=0.0, new_max=1.0):
    """Normalize a list of numbers to the range between new_min and new_max.

    Parameters
    ----------
    values : sequence[float]
        The data to be normalized.
    new_min : float, optional
        The new minimum of the data.
    new_max : float, optional
        The new maximum of the data.

    Returns
    -------
    list[float]
        A new list of floats mapped to the range `new_min`, `new_max`.

    Examples
    --------
    >>> data = list(range(5, 15))
    >>> data = normalize_values(data)
    >>> min(data)
    0.0
    >>> max(data)
    1.0

    """
    old_max = max(values)
    old_min = min(values)
    old_range = (old_max - old_min)
    new_range = (new_max - new_min)
    return [(((value - old_min) * new_range) / old_range) + new_min for value in values]


def remap_values(values, target_min=0.0, target_max=1.0, original_min=None, original_max=None):
    """Maps a list of numbers from one domain to another.

    Parameters
    ----------
    values : sequence[int | float]
        The value to remap
    target_min : int | float, optional
        The minimun value of the target domain.
    target_max : int | float, optional
        The maximum value of the target domain.
    original_min : int | float, optional
        The minimun value of the original domain, other than the actual minimum value.
    original_max : int | float, optional
        The maximum value of the original domain, other than the actual maximum value.

    Returns
    -------
    list[float]
        The remaped list of values.

    """
    if original_min is None:
        original_min = min(values)
    if original_max is None:
        original_max = max(values)
    original_range = original_max - original_min
    target_range = target_max - target_min
    ratio = target_range / original_range
    return [target_min + ((value - original_min) * ratio) for value in values]


def meshgrid(x, y, indexing='xy'):
    """Construct coordinate matrices from two coordinate vectors.

    Parameters
    ----------
    x : list[float]
        The values of the "x axis" of the grid.
    y : list[float]
        The values of the "y axis" of the grid.
    indexing : Literal['xy', 'ij'], optional
        The indexing strategy determines the structure of the output.

    Returns
    -------
    list[list[float]]
        The X values of the coordinate grid.
    list[list[float]]
        The Y values of the coordinate grid.

    Notes
    -----
    The output of this function consists of two "matrices", `X` and `Y`.
    The structure of the matrices is determined by the choice of `indexing`.
    Assuming ``m = len(x)`` and ``n = len(y)``.
    If `indexing` is ``'xy'``,
    the shape of both matrices is ``(n, m)``,
    with `X` containing the elements of `x` in its rows, and `Y` the elements of `y` in its columns.
    If `indexing` is ``'ij'``,
    the shape of both matrices is ``(m, n)``,
    with `X` containing the elements of `x` in its columns, and `Y` the elements of `y` in its rows.

    References
    ----------
    This function mimicks the functionality of ``numpy.meshgrid`` [1]_, but in a simpler form.

    .. [1] `numpy.meshgrid`.
           Available at https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html

    Examples
    --------
    >>> from compas.utilities import linspace, meshgrid
    >>> x = list(linspace(0, 1, 3))
    >>> y = list(linspace(0, 1, 2))

    >>> X, Y = meshgrid(x, y)
    >>> X
    [[0.0, 0.5, 1.0], [0.0, 0.5, 1.0]]
    >>> Y
    [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]

    >>> X, Y = meshgrid(x, y, 'ij')
    >>> X
    [[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]]
    >>> Y
    [[0.0, 1.0], [0.0, 1.0], [0.0, 1.0]]

    """
    x = list(x)
    y = list(y)
    if indexing == 'xy':
        X = [[x[j] for j in range(len(x))] for i in range(len(y))]
        Y = [[y[i] for j in range(len(x))] for i in range(len(y))]
    else:
        X = [[x[i] for j in range(len(y))] for i in range(len(x))]
        Y = [[y[j] for j in range(len(y))] for i in range(len(x))]
    return X, Y


def linspace(start, stop, num=50):
    """Generate a sequence of evenly spaced numbers over a specified interval.

    Parameters
    ----------
    start : float
        The start value of the sequence.
    stop : float
        The end value of the sequence.
    num : int
        The number of elements in the sequence.

    Yields
    ------
    float
        The next value in the sequence.

    References
    ----------
    This function mimicks the functionality of ``numpy.linspace`` [1]_, but in a simpler form.

    .. [1] ``numpy.linspace`` Available at https://numpy.org/doc/stable/reference/generated/numpy.linspace.html

    Examples
    --------
    >>> from compas.utilities import linspace
    >>> list(linspace(0, 1, 3))
    [0.0, 0.5, 1.0]

    """
    step = (stop - start) / (num - 1)
    for i in range(num):
        yield start + i * step


def flatten(list_of_lists):
    """Flatten one level of nesting.

    Parameters
    ----------
    list_of_lists : iterable[iterable[Any]]
        An iterable object containing other iterable objects.

    Returns
    -------
    iterable
        An iterable containing the elements of the elements of the nested iterables.

    """
    return chain.from_iterable(list_of_lists)


def pairwise(iterable):
    """Returns a sliding window of size 2 over the data of the iterable.

    Parameters
    ----------
    iterable : iterable
        A sequence of items.

    Yields
    ------
    tuple
        Two items per iteration, if there are at least two items in the iterable.

    Examples
    --------
    >>> for a, b in pairwise(range(5)):
    ...     print(a, b)
    ...
    0 1
    1 2
    2 3
    3 4

    """
    a, b = tee(iterable)
    next(b, None)
    return zip(a, b)


def window(seq, n=2):
    """Returns a sliding window (of width n) over the data from the iterable.

    Parameters
    ----------
    seq : iterable
        A sequence of items.
    n : int, optional
        The width of the sliding window.

    Yields
    ------
    tuple
        A tuple of size `n` at every iteration,
        if there are at least `n` items in the sequence.

    Examples
    --------
    >>> for view in window(range(10), 3):
    ...     print(view)
    ...
    (0, 1, 2)
    (1, 2, 3)
    (2, 3, 4)
    (3, 4, 5)
    (4, 5, 6)
    (5, 6, 7)
    (6, 7, 8)
    (7, 8, 9)

    """
    it = iter(seq)
    result = tuple(islice(it, n))
    if len(result) == n:
        yield result
    for elem in it:
        result = result[1:] + (elem,)
        yield result


def iterable_like(target, reference, fillvalue=None):
    """Creates an iterator from a reference object with size equivalent to that of a target iterable.

    Parameters
    ----------
    target : iterable
        An iterable to be matched in size.
    reference: iterable
        The iterable containing the original data.
    fillvalue : object, optional
        A value to replace missing items.

    Yields
    ------
    object
        The next value in the iterator.

    Notes
    -----
    Values will be yielded one at a time until the reference iterable is exhausted.
    If `target` contains more values than `reference`, `fillvalue` will be used to cover the difference.
    Otherwise, only the same number of items from `reference` will be yielded as there would have been from `target`.

    Examples
    --------
    >>> keys = [0, 1, 2, 3]
    >>> color = (255, 0, 0)
    >>> list(iterable_like(keys, [color], color))
    [(255, 0, 0), (255, 0, 0), (255, 0, 0), (255, 0, 0)]
    >>> list(iterable_like(color, keys))
    [0, 1, 2]

    """
    target, counter = tee(target)
    zipped = zip_longest(target, reference, fillvalue=fillvalue)
    for _ in counter:
        yield next(zipped)[1]


# ==============================================================================
# Other
# ==============================================================================


def take(n, iterable):
    """Return the first n items of the iterable as a list.

    Parameters
    ----------
    n : int
        The number of items.
    iterable : iterable
        An iterable object.

    Returns
    -------
    list
        A list with the first `n` items of `iterable`.

    Examples
    --------
    >>> take(5, range(100))
    [0, 1, 2, 3, 4]
    """
    return list(islice(iterable, n))


def nth(iterable, n, default=None):
    """Returns the nth item or a default value"""
    return next(islice(iterable, n, None), default)


def padnone(iterable):
    """Returns the sequence elements and then returns None indefinitely.

    Useful for emulating the behavior of the built-in map() function.
    """
    return chain(iterable, repeat(None))


def grouper(iterable, n, fillvalue=None):
    """Collect data into fixed-length chunks or blocks.
    """
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)


# def random_product(*args, repeat=1):
#     """Random selection from itertools.product(*args, **kwds)"""
#     pools = [tuple(pool) for pool in args] * repeat
#     return tuple(random.choice(pool) for pool in pools)


# def random_permutation(iterable, r=None):
#     """Random selection from itertools.permutations(iterable, r)"""
#     pool = tuple(iterable)
#     r = len(pool) if r is None else r
#     return tuple(random.sample(pool, r))


# def random_combination(iterable, r):
#     """Random selection from itertools.combinations(iterable, r)"""
#     pool = tuple(iterable)
#     n = len(pool)
#     indices = sorted(random.sample(range(n), r))
#     return tuple(pool[i] for i in indices)


# def random_combination_with_replacement(iterable, r):
#     """Random selection from itertools.combinations_with_replacement(iterable, r)"""
#     pool = tuple(iterable)
#     n = len(pool)
#     indices = sorted(random.randrange(n) for i in range(r))
#     return tuple(pool[i] for i in indices)


# src/compas/utilities/maps.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas


__all__ = [
    'geometric_key',
    'reverse_geometric_key',
    'geometric_key_xy',
]


def geometric_key(xyz, precision=None, sanitize=True):
    """Convert XYZ coordinates to a string that can be used as a dict key.

    Parameters
    ----------
    xyz : list[float]
        The XYZ coordinates.
    precision : str, optional
        A formatting option that specifies the precision of the
        individual numbers in the string.
        Supported values are any float precision (e.g. ``'3f'``), or decimal integer (``'d'``).
        Default is ``None``, in which case the global precision setting will be used (:attr:`compas.PRECISION`).
    sanitize : bool, optional
        If True, minus signs ("-") will be removed from values that are equal to zero up to the given precision.

    Returns
    -------
    str
        The string representation of the given coordinates.

    See also
    --------
    geometric_key_xy

    Examples
    --------
    >>> from math import pi
    >>> geometric_key([pi, pi, pi])
    '3.142,3.142,3.142'

    """
    x, y, z = xyz
    if not precision:
        precision = compas.PRECISION
    if precision == 'd':
        return '{0},{1},{2}'.format(int(x), int(y), int(z))
    if sanitize:
        minzero = "-{0:.{1}}".format(0.0, precision)
        if "{0:.{1}}".format(x, precision) == minzero:
            x = 0.0
        if "{0:.{1}}".format(y, precision) == minzero:
            y = 0.0
        if "{0:.{1}}".format(z, precision) == minzero:
            z = 0.0
    return '{0:.{3}},{1:.{3}},{2:.{3}}'.format(x, y, z, precision)


def reverse_geometric_key(gkey):
    """Reverse a geometric key string into xyz coordinates.

    Parameters
    ----------
    gkey : str
        A geometric key.

    Returns
    -------
    list[float]
        A list of XYZ coordinates.

    See Also
    --------
    geometric_key

    Examples
    --------
    >>> from math import pi
    >>> xyz = [pi, pi, pi]
    >>> gkey = geometric_key(xyz)
    >>> reverse_geometric_key(gkey)
    [3.142, 3.142, 3.142]

    """
    xyz = gkey.split(',')
    return [float(i) for i in xyz]


def geometric_key_xy(xy, precision=None, sanitize=True):
    """Convert XY coordinates to a string that can be used as a dict key.

    Parameters
    ----------
    xy : list[float]
        The XY(Z) coordinates.
    precision : str, optional
        A formatting option that specifies the precision of the
        individual numbers in the string.
        Supported values are any float precision (e.g. ``'3f'``), or decimal integer (``'d'``).
        Default is ``None``, inwhich case the global precision setting will be used (``compas.PRECISION``).
    sanitize : bool, optional
        If True, minus signs ("-") will be removed from values that are equal to zero up to the given precision.

    Returns
    -------
    str
        The string representation of the given coordinates.

    See also
    --------
    geometric_key

    Examples
    --------
    >>> from math import pi
    >>> geometric_key_xy([pi, pi, pi])
    '3.142,3.142'

    """
    x, y = xy[:2]
    if not precision:
        precision = compas.PRECISION
    if precision == 'd':
        return '{0},{1}'.format(int(x), int(y))
    if sanitize:
        minzero = "-{0:.{1}}".format(0.0, precision)
        if "{0:.{1}}".format(x, precision) == minzero:
            x = 0.0
        if "{0:.{1}}".format(y, precision) == minzero:
            y = 0.0
    return '{0:.{2}},{1:.{2}}'.format(x, y, precision)


# src/compas/utilities/remote.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import os
# import io

# import compas

try:
    # from urllib.request import urlopen
    from urllib.request import urlretrieve
except ImportError:
    # from urllib2 import urlopen
    from urllib import urlretrieve

# try:
#     from PIL import Image
# except ImportError:
#     if compas.is_windows():
#         compas.raise_if_not_ironpython()
#     elif not compas.is_mono():
#         raise


__all__ = ['download_file_from_remote']


def download_file_from_remote(source, target, overwrite=True):
    """Download a file from a remote source and save it to a local destination.

    Parameters
    ----------
    source : str
        The url of the source file.
    target : str
        The path of the local destination.
    overwrite : bool, optional
        If True, overwrite `target` if it already exists.

    Examples
    --------
    .. code-block:: python

        import os
        import compas
        from compas.utilities.remote import download_file_from_remote

        source = 'https://raw.githubusercontent.com/compas-dev/compas/main/data/faces.obj'
        target = os.path.join(compas.APPDATA, 'data', 'faces.obj')

        download_file_from_remote(source, target)

    """
    parent = os.path.abspath(os.path.dirname(target))

    if not os.path.exists(parent):
        os.makedirs(parent)

    if not os.path.isdir(parent):
        raise Exception('The target path is not a valid file path: {}'.format(target))

    if not os.access(parent, os.W_OK):
        raise Exception('The target path is not writable: {}'.format(target))

    if not os.path.exists(target):
        urlretrieve(source, target)
    else:
        if overwrite:
            urlretrieve(source, target)


# def download_image_from_remote(source, target, show=False):
#     """Download an image from a remote source and save it to a local destination.

#     Parameters
#     ----------
#     source : str
#         The url of the source file.
#     target : str
#         The path of the local destination.
#     show : bool, optional
#         Show the downloaded image.
#         Default is ``False``

#     Examples
#     --------
#     .. code-block:: python

#         import os
#         import compas
#         from compas.utilities.remote import download_image_from_remote

#         source = 'http://block.arch.ethz.ch/brg/images/cache/dsc02360_ni-2_cropped_1528706473_624x351.jpg'
#         target = os.path.join(compas.APPDATA, 'data', 'theblock.jpg')

#         download_image_from_remote(source, target, show=True)

#     """
#     # response = requests.get(source)
#     # response.raise_for_status()

#     response = urlopen(source)
#     image = Image.open(io.BytesIO(response.read()))

#     if show:
#         image.show()
#     image.save(target)


# src/compas/utilities/ssh.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

try:
    from paramiko import AutoAddPolicy
    from paramiko import SSHClient
except ImportError:
    pass

import os


__all__ = ['SSH']


class SSH(object):
    """Initialse an SSH object.

    Parameters
    ----------
    server : str
        ssh server address.
    username : str
        Username.

    """

    def __init__(self, server, username):
        self.server = server
        self.username = username
        self.client = self.create_client()

    def create_client(self):
        """Create an SSH client with Paramiko.

        Returns
        -------
        SSHClient
            ssh client object.

        """
        client = SSHClient()
        client.set_missing_host_key_policy(AutoAddPolicy())
        try:
            client.connect(self.server, username=self.username)
            print('\n***** Connected to server: {0} with username: {1}'.format(self.server, self.username))
        except Exception:
            print('\n***** Connection failed')
        return client

    def close(self):
        """Close the SSH object.

        Returns
        -------
        None

        """
        self.client.close()
        print('\n***** SSH connection closed')

    def receive_file(self, remote_file, local_file):
        """Recieve a remote file from the server.

        Parameters
        ----------
        remote_file : str
            Path of the remote file to recieve.
        local_file : str
            Path to save the local file to.

        Returns
        -------
        None

        """
        command = 'scp {0}@{1}:{2} {3}'.format(self.username, self.server, remote_file, local_file)
        self.local_command(command)

    def send_file(self, local_file):
        """Send a local file to the server.

        Parameters
        ----------
        local_file : str
            Path of the local file to send.

        Returns
        -------
        None

        """
        command = 'scp {0} {1}@{2}:'.format(local_file, self.username, self.server)
        self.local_command(command=command)

    def send_folder(self, local_folder):
        """Send a local folder to the server.

        Parameters
        ----------
        local_folder : str
            Path of the local folder to send.

        Returns
        -------
        None

        """
        command = 'scp -r {0} {1}@{2}:'.format(local_folder, self.username, self.server)
        self.local_command(command=command)

    def sync_folder(self, local_folder, remote_folder):
        """Sync using rsync, a local folder to a remote folder on the server.

        Parameters
        ----------
        local_folder : str
            Path of the local folder to sync from.
        remote_folder : str
            Path of the remote folder to sync to.

        Returns
        -------
        None

        """
        command = 'rsync -Pa {0} {1}@{2}:{3}'.format(local_folder, self.username, self.server, remote_folder)
        self.local_command(command=command)

    @staticmethod
    def local_command(command, folder=None):
        """Enter a local BASH command.

        Parameters
        ----------
        command : str
            The command to execute on the local system.
        folder : str
            The local folder to execute the command from.

        Returns
        -------
        None

        """
        print('\n***** Executing local command: {0}'.format(command))
        if folder:
            os.chdir(folder)
        os.system(command)
        print('***** Command executed')

    def server_command(self, command):
        """Send a BASH command to run on the server.

        Parameters
        ----------
        command : str
            The command to run on the remote system.

        Returns
        -------
        None

        """
        print('\n***** Executing server command: {0}\n'.format(command))
        stdin, stdout, stderr = self.client.exec_command(command)
        for line in stdout.readlines():
            print(line)
        for line in stderr.readlines():
            print(line)
        print('***** Command executed')


# src/compas/utilities/xfunc.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import os
import json
import tempfile

import compas
import compas._os

from compas.utilities import DataEncoder
from compas.utilities import DataDecoder

try:
    import cPickle as pickle
except ImportError:
    import pickle

try:
    from subprocess import Popen
    from subprocess import PIPE
except ImportError:
    try:
        from System.Diagnostics import Process
    except ImportError:
        compas.raise_if_ironpython()


__all__ = ['XFunc']


WRAPPER = """
import os
import sys
import importlib

import json

try:
    import cPickle as pickle
except Exception:
    import pickle

try:
    from cStringIO import StringIO
except Exception:
    from io import StringIO

import cProfile
import pstats
import traceback

from compas.utilities import DataEncoder
from compas.utilities import DataDecoder

basedir    = sys.argv[1]
funcname   = sys.argv[2]
ipath      = sys.argv[3]
opath      = sys.argv[4]
serializer = sys.argv[5]

if serializer == 'json':
    with open(ipath, 'r') as fo:
        idict = json.load(fo, cls=DataDecoder)
else:
    with open(ipath, 'rb') as fo:
        idict = pickle.load(fo)

try:
    args   = idict['args']
    kwargs = idict['kwargs']

    profile = cProfile.Profile()
    profile.enable()

    sys.path.insert(0, basedir)
    parts = funcname.split('.')

    if len(parts) > 1:
        mname = '.'.join(parts[:-1])
        fname = parts[-1]
        m = importlib.import_module(mname)
        f = getattr(m, fname)
    else:
        raise Exception('Cannot import the function because no module name is specified.')

    r = f(*args, **kwargs)

    profile.disable()

    stream = StringIO()
    stats  = pstats.Stats(profile, stream=stream)
    # stats.strip_dirs()
    stats.sort_stats(1)
    stats.print_stats(20)

except Exception:
    odict = {}
    odict['error']      = traceback.format_exc()
    odict['data']       = None
    odict['profile']    = None

else:
    odict = {}
    odict['error']      = None
    odict['data']       = r
    odict['profile']    = stream.getvalue()

if serializer == 'json':
    with open(opath, 'w+') as fo:
        json.dump(odict, fo, cls=DataEncoder)
else:
    with open(opath, 'wb+') as fo:
        # pickle.dump(odict, fo, protocol=pickle.HIGHEST_PROTOCOL)
        pickle.dump(odict, fo, protocol=2)

"""


class XFunc(object):
    """Wrapper for functions that turns them into externally run processes.

    Parameters
    ----------
    funcname : str
        The full name of the function.
    basedir : str, optional
        A directory that should be added to the PYTHONPATH such that the function can be found.
        Default is the curent directory.
    tmpdir : str, optional
        A directory that should be used for storing the IO files.
        Default is the current directory.
    delete_files : bool, optional
        If True, the IO files will be deleted afterwards.
    verbose : bool, optional
        If True, information about the process will be displayed to the user.
    callback : callable, optional
        A function to be called eveytime the wrapped function prints output.
        The first parameter passed to this function is the line printed by the
        wrapped function. Additional parameters can be defined using `callback_args`.
    callback_args : tuple, optional
        Additional parameter for the callback function.
    python : str, optional
        The Python executable.
        This can be a path to a specific executable (e.g. ``'/opt/local/bin/python'``)
        or the name of an executable registered on the system `PATH` (e.g. ``'pythonw'``).
    paths : list, optional
        A list of paths to be added to the `PYTHONPATH` by the subprocess.
    serializer : {'json', 'pickle'}, optional
        The serialization mechnanism to be used to pass data between the caller and the subprocess.

    Attributes
    ----------
    data : object
        The object returned by the wrapped function.
    profile : str
        A profile of the call to the wrapped function.
    error : str
        A traceback of the exception raised during the wrapped function call.

    Methods
    -------
    __call__(*args, **kwargs)
        Call the wrapped function with the apropriate/related arguments and keyword arguments.

    Notes
    -----
    To use the Python executable of a virtual environment, simply assign the path
    to that executable to the `python` parameter. For example

    .. code-block:: python

        fd_numpy = XFunc('compas.numerical.fd_numpy', python='/Users/brg/environments/py2/python')

    Examples
    --------
    :mod:`compas.numerical` provides an implementation of the Force Density Method that
    is based on Numpy and Scipy. This implementation is not directly available in
    Rhino because Numpy and Scipy are not available for IronPython.

    With :class:`compas.utilities.XFunc`, :func:`compas.numerical.fd_numpy` can be easily
    wrapped in an external process and called as if it would be directly available.

    .. code-block:: python

        import compas
        import compas_rhino

        from compas_rhino.artists import MeshArtist
        from compas.datastructures import Mesh
        from compas.utilities import XFunc

        # make the function available as a wrapped function with the same call signature and return value as the original.
        fd_numpy = XFunc('compas.numerical.fd_numpy')

        mesh = Mesh.from_obj(compas.get('faces.obj'))

        mesh.update_default_vertex_attributes({'is_fixed': False, 'px': 0.0, 'py': 0.0, 'pz': 0.0})
        mesh.update_default_edge_attributes({'q': 1.0})

        for key, attr in mesh.vertices(True):
            attr['is_fixed'] = mesh.vertex_degree(key) == 2

        key_index = mesh.key_index()
        vertices  = mesh.vertices_attributes('xyz')
        edges     = [(key_index[u], key_index[v]) for u, v in mesh.edges()]
        fixed     = [key_index[key] for key in mesh.vertices_where({'is_fixed': True})]
        q         = mesh.edges_attribute('q')
        loads     = mesh.vertices_attributes(('px', 'py', 'pz'))

        xyz, q, f, l, r = fd_numpy(vertices, edges, fixed, q, loads)

        for key, attr in mesh.vertices(True):
            attr['x'] = xyz[key][0]
            attr['y'] = xyz[key][1]
            attr['z'] = xyz[key][2]

        artist = MeshArtist(mesh)
        artist.draw_vertices()
        artist.draw_edges()

    """

    def __init__(self, funcname, basedir='.', tmpdir=None, delete_files=True,
                 verbose=True, callback=None, callback_args=None, python=None,
                 paths=None, serializer='json',
                 argtypes=None, kwargtypes=None, restypes=None):
        self._basedir = None
        self._tmpdir = None
        self._callback = None
        self._python = None
        self._serializer = None
        self.funcname = funcname
        self.basedir = basedir
        self.tmpdir = tmpdir or tempfile.mkdtemp('compas_xfunc')
        self.delete_files = delete_files
        self.verbose = verbose
        self.callback = callback
        self.callback_args = callback_args
        self.python = compas._os.select_python(python)
        self.paths = paths or []
        self.serializer = serializer
        self.argtypes = argtypes
        self.kwargtypes = kwargtypes
        self.restypes = restypes
        self.data = None
        self.profile = None
        self.error = None

    @property
    def basedir(self):
        return self._basedir

    @basedir.setter
    def basedir(self, basedir):
        if not os.path.isdir(basedir):
            raise Exception("basedir is not a directory: %s" % basedir)
        self._basedir = os.path.abspath(basedir)

    @property
    def tmpdir(self):
        return self._tmpdir

    @tmpdir.setter
    def tmpdir(self, tmpdir):
        if not os.path.isdir(tmpdir):
            raise Exception("tmpdir is not a directory: %s" % tmpdir)
        if not os.access(tmpdir, os.W_OK):
            raise Exception("You do not have write access to 'tmpdir'. Please set the 'tmpdir' attribute to a different directory.")
        self._tmpdir = os.path.abspath(tmpdir)

    @property
    def callback(self):
        return self._callback

    @callback.setter
    def callback(self, callback):
        if callback:
            if not callable(callback):
                callback = None
        self._callback = callback

    @property
    def python(self):
        return self._python

    @python.setter
    def python(self, python):
        self._python = python

    @property
    def serializer(self):
        """{'json', 'pickle'}: Which serialization mechanism to use."""
        return self._serializer

    @serializer.setter
    def serializer(self, serializer):
        if serializer not in ('json', 'pickle'):
            raise Exception("*serializer* should be one of {'json', 'pickle'}.")
        self._serializer = serializer

    @property
    def ipath(self):
        return os.path.join(self.tmpdir, '%s.in' % self.funcname)

    @property
    def opath(self):
        return os.path.join(self.tmpdir, '%s.out' % self.funcname)

    def __call__(self, *args, **kwargs):
        """Make a call to the wrapped function.

        Parameters
        ----------
        **args : list
            Positional arguments to be passed to the wrapped function.
        **kwargs : dict, optional
            Named arguments to be passed to the wrapped function.

        Returns
        -------
        object or None
            The data returned by the wrapped call.
            The type of the return value depends on the implementation of the wrapped function.
            If something went wrong the value is ``None``.
            In this case, check the :attr:`XFunc.error` for more information.

        """
        # if self.argtypes:
        #     args = [arg for arg in args]

        # if self.kwargtypes:
        #     kwargs = {name: value for name, value in kwargs.items()}

        idict = {
            'args': args,
            'kwargs': kwargs,
            # 'argtypes': self.argtypes,
            # 'kwargtypes': self.kwargtypes,
            # 'restypes': self.restypes
        }

        if self.serializer == 'json':
            with open(self.ipath, 'w+') as fo:
                json.dump(idict, fo, cls=DataEncoder)
        else:
            with open(self.ipath, 'wb+') as fo:
                pickle.dump(idict, fo, protocol=2)

        with open(self.opath, 'w+') as fh:
            fh.write('')

        env = compas._os.prepare_environment()
        args = [WRAPPER, self.basedir, self.funcname, self.ipath, self.opath, self.serializer]

        try:
            Popen

        except NameError:
            process = Process()
            for name in env:
                if process.StartInfo.EnvironmentVariables.ContainsKey(name):
                    process.StartInfo.EnvironmentVariables[name] = env[name]
                else:
                    process.StartInfo.EnvironmentVariables.Add(name, env[name])
            process.StartInfo.UseShellExecute = False
            process.StartInfo.RedirectStandardOutput = True
            process.StartInfo.RedirectStandardError = True
            process.StartInfo.FileName = self.python
            process.StartInfo.Arguments = '-u -c "{0}" {1} {2} {3} {4} {5}'.format(*args)
            process.Start()
            process.WaitForExit()

            while True:
                line = process.StandardOutput.ReadLine()
                if not line:
                    break
                line = line.strip()
                if self.callback:
                    self.callback(line, self.callback_args)
                if self.verbose:
                    print(line)

            # stderr = p.StandardError.ReadToEnd()

        else:
            process_args = [self.python, '-u', '-c'] + args

            process = Popen(process_args, stderr=PIPE, stdout=PIPE, env=env)

            while process.poll() is None:
                line = process.stdout.readline().strip()
                if self.callback:
                    self.callback(line, self.callback_args)
                if self.verbose:
                    print(line)

        if self.serializer == 'json':
            with open(self.opath, 'r') as fo:
                odict = json.load(fo, cls=DataDecoder)
        else:
            with open(self.opath, 'rb') as fo:
                odict = pickle.load(fo)

        self.data = odict['data']
        self.profile = odict['profile']
        self.error = odict['error']

        if self.delete_files:
            try:
                os.remove(self.ipath)
            except OSError:
                pass
            try:
                os.remove(self.opath)
            except OSError:
                pass

        if self.error:
            raise Exception(self.error)

        return self.data


# src/compas_blender/artists/__init__.py

"""
********************************************************************************
artists
********************************************************************************

.. currentmodule:: compas_blender.artists

.. rst-class:: lead

Artists for visualizing (painting) COMPAS geometry, robots, and data structures in Blender.
The artists in this package provide plugins for the pluggable methods of the base artists defined in :mod:`compas.artists`.
They can be used directly from here (:mod:`compas_blender.artists`), or through the base artist :class:`compas.artists.Artist`.


Primitive Artists
=================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    CircleArtist
    FrameArtist
    LineArtist
    PointArtist
    PolygonArtist
    PolylineArtist
    VectorArtist


Shape Artists
=============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    BoxArtist
    CapsuleArtist
    ConeArtist
    CylinderArtist
    PolyhedronArtist
    SphereArtist


Datastructure Artists
=====================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    MeshArtist
    NetworkArtist


Robot Artist
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    RobotModelArtist


Base Classes
============

.. autosummary::
    :toctree: generated/

    BlenderArtist

"""

import compas_blender

from compas.plugins import plugin
from compas.artists import Artist

from compas.geometry import Box
from compas.geometry import Capsule
from compas.geometry import Circle
from compas.geometry import Cone
from compas.geometry import Cylinder
from compas.geometry import Frame
from compas.geometry import Line
from compas.geometry import Point
from compas.geometry import Polygon
from compas.geometry import Polyhedron
from compas.geometry import Polyline
from compas.geometry import Sphere
from compas.geometry import Torus
from compas.geometry import Vector
from compas.datastructures import Mesh
from compas.datastructures import Network
from compas.robots import RobotModel

from .artist import BlenderArtist
from .boxartist import BoxArtist
from .capsuleartist import CapsuleArtist
from .circleartist import CircleArtist
from .coneartist import ConeArtist
from .cylinderartist import CylinderArtist
from .frameartist import FrameArtist
from .lineartist import LineArtist
from .meshartist import MeshArtist
from .networkartist import NetworkArtist
from .pointartist import PointArtist
from .polygonartist import PolygonArtist
from .polyhedronartist import PolyhedronArtist
from .polylineartist import PolylineArtist
from .robotmodelartist import RobotModelArtist
from .sphereartist import SphereArtist
from .torusartist import TorusArtist
from .vectorartist import VectorArtist


@plugin(category='drawing-utils', pluggable_name='clear', requires=['bpy'])
def clear_blender():
    print('doing it')
    compas_blender.clear()


@plugin(category='drawing-utils', pluggable_name='redraw', requires=['bpy'])
def redraw_blender():
    compas_blender.redraw()


@plugin(category='factories', requires=['bpy'])
def register_artists():
    Artist.register(Box, BoxArtist, context='Blender')
    Artist.register(Capsule, CapsuleArtist, context='Blender')
    Artist.register(Circle, CircleArtist, context='Blender')
    Artist.register(Cone, ConeArtist, context='Blender')
    Artist.register(Cylinder, CylinderArtist, context='Blender')
    Artist.register(Frame, FrameArtist, context='Blender')
    Artist.register(Line, LineArtist, context='Blender')
    Artist.register(Mesh, MeshArtist, context='Blender')
    Artist.register(Network, NetworkArtist, context='Blender')
    Artist.register(Point, PointArtist, context='Blender')
    Artist.register(Polygon, PolygonArtist, context='Blender')
    Artist.register(Polyhedron, PolyhedronArtist, context='Blender')
    Artist.register(Polyline, PolylineArtist, context='Blender')
    Artist.register(RobotModel, RobotModelArtist, context='Blender')
    Artist.register(Sphere, SphereArtist, context='Blender')
    Artist.register(Torus, TorusArtist, context='Blender')
    Artist.register(Vector, VectorArtist, context='Blender')
    print('Blender Artists registered.')


__all__ = [
    'BlenderArtist',
    'BoxArtist',
    'CapsuleArtist',
    'CircleArtist',
    'ConeArtist',
    'CylinderArtist',
    'FrameArtist',
    'LineArtist',
    'MeshArtist',
    'NetworkArtist',
    'PointArtist',
    'PolygonArtist',
    'PolyhedronArtist',
    'PolylineArtist',
    'RobotModelArtist',
    'SphereArtist',
    'TorusArtist',
    'VectorArtist',
]


# src/compas_blender/artists/artist.py

from typing import Union
from typing import Optional
from typing import Any

import bpy
import compas_blender

from compas.artists import Artist


class BlenderArtist(Artist):
    """Base class for all Blender artists.

    Parameters
    ----------
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection the object(s) created by the artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas.artists.Artist` for more info.

    Attributes
    ----------
    collection : :blender:`bpy.types.Collection`
        The collection containing the object(s) created by this artist.

    """

    def __init__(self,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):
        # Initialize collection before even calling super because other classes depend on that
        self._collection = None
        self.collection = collection
        super().__init__(**kwargs)

    @property
    def collection(self) -> bpy.types.Collection:
        return self._collection

    @collection.setter
    def collection(self, value: Union[str, bpy.types.Collection]):
        if isinstance(value, bpy.types.Collection):
            self._collection = value
        elif isinstance(value, str):
            self._collection = compas_blender.create_collection(value)
        else:
            raise Exception('Collection must be of type `str` or `bpy.types.Collection`.')


# src/compas_blender/artists/boxartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy
import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import Box
from compas.artists import ShapeArtist
from .artist import BlenderArtist


class BoxArtist(BlenderArtist, ShapeArtist):
    """Artist for drawing box shapes in Blender.

    Parameters
    ----------
    box : :class:`compas.geometry.Box`
        A COMPAS box.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.ShapeArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Box
        from compas_blender.artists import BoxArtist

        box = Box.from_width_height_depth(1, 1, 1)

        artist = BoxArtist(box)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Box
        from compas.artists import Artist

        box = Box.from_width_height_depth(1, 1, 1)

        artist = Artist(box)
        artist.draw()

    """

    def __init__(self,
                 box: Box,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(shape=box, collection=collection or box.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None) -> List[bpy.types.Object]:
        """Draw the box associated with the artist.

        Parameters
        ----------
        color : rgb-tuple, optional
            The RGB color of the box.
            The default color is :attr:`compas.artists.ShapeArtist.color`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]
            The object(s) created in Blender to represent the box.

        """
        color = color or self.color
        vertices, faces = self.shape.to_vertices_and_faces()
        obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection)
        return [obj]


# src/compas_blender/artists/capsuleartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import Capsule
from compas.artists import ShapeArtist
from .artist import BlenderArtist


class CapsuleArtist(BlenderArtist, ShapeArtist):
    """Artist for drawing capsule shapes in Blender.

    Parameters
    ----------
    capsule : :class:`compas.geometry.Capsule`
        A COMPAS capsule.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.ShapeArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Capsule
        from compas_blender.artists import CapsuleArtist

        capsule = Capsule(([0, 0, 0], [1, 0, 0]), 0.3)

        artist = CapsuleArtist(capsule)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Capsule
        from compas.artists import Artist

        capsule = Capsule(([0, 0, 0], [1, 0, 0]), 0.3)

        artist = Artist(capsule)
        artist.draw()

    """

    def __init__(self,
                 capsule: Capsule,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(shape=capsule, collection=collection or capsule.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None, u: int = None, v: int = None) -> List[bpy.types.Object]:
        """Draw the capsule associated with the artist.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the capsule.
            The default color is :attr:`compas.artists.ShapeArtist.color`.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`CapsuleArtist.u`.
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`CapsuleArtist.v`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]
            The objects created in Blender.

        """
        u = u or self.u
        v = v or self.v
        color = color or self.color
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection)
        return [obj]


# src/compas_blender/artists/circleartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import add_vectors
from compas.geometry import Circle
from compas.artists import PrimitiveArtist
from .artist import BlenderArtist


class CircleArtist(BlenderArtist, PrimitiveArtist):
    """Artist for drawing circles in Blender.

    Parameters
    ----------
    circle : :class:`compas.geometry.Circle`
        A COMPAS circle.
    collection :  str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.PrimitiveArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Plane, Circle
        from compas_blender.artists import CircleArtist

        circle = Circle(Plane([0, 0, 0], [0,, 0, 1]), 1.0)

        artist = CircleArtist(circle)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Plane, Circle
        from compas.artists import Artist

        circle = Circle(Plane([0, 0, 0], [0,, 0, 1]), 1.0)

        artist = Artist(circle)
        artist.draw()

    """

    def __init__(self,
                 circle: Circle,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):
        super().__init__(primitive=circle, collection=collection or circle.name, **kwargs)

    def draw(self,  color: Optional[RGBColor] = None, show_point=False, show_normal=False) -> List[bpy.types.Object]:
        """Draw the circle.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the capsule.
            The default color is :attr:`compas.artists.PrimitiveArtist.color`.
        show_point : bool, optional
            If True, also draw the center point of the circle.
        show_normal : bool, optional
            If True, also draw the normal vector of the circle.

        Returns
        -------
        list[:blender:`bpy.types.Object`]
            The objects created in Blender.

        """
        color = color or self.color
        point = self.primitive.plane.point
        normal = self.primitive.plane.normal
        plane = point, normal
        radius = self.primitive.radius
        objects = []
        if show_point:
            points = [{'pos': point, 'color': color, 'name': self.primitive.name, 'radius': 0.01}]
            objects += compas_blender.draw_points(points, collection=self.collection)
        if show_normal:
            end = add_vectors(point, normal)
            lines = [{'start': point, 'end': end, 'color': color, 'name': self.primitive.name}]
            objects += compas_blender.draw_lines(lines, collection=self.collection)
        circles = [{'plane': plane, 'radius': radius, 'color': color, 'name': self.primitive.name}]
        objects += compas_blender.draw_circles(circles, collection=self.collection)
        return objects


# src/compas_blender/artists/coneartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import Cone
from compas.artists import ShapeArtist
from .artist import BlenderArtist


class ConeArtist(BlenderArtist, ShapeArtist):
    """Artist for drawing cone shapes in Blender.

    Parameters
    ----------
    cone : :class:`compas.geometry.Cone`
        A COMPAS cone.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.ShapeArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Plane, Circle, Cone
        from compas_blender.artists import ConeArtist

        cone = Cone(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0)

        artist = ConeArtist(cone)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Plane, Circle, Cone
        from compas.artists import Artist

        cone = Cone(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0)

        artist = Artist(cone)
        artist.draw()

    """

    def __init__(self,
                 cone: Cone,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(shape=cone, collection=collection or cone.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None, u: int = None) -> List[bpy.types.Object]:
        """Draw the cone associated with the artist.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the cone.
            The default color is :attr:`compas.artists.ShapeArtist.color`.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`ConeArtist.u`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]
            The objects created in Blender.

        """
        u = u or self.u
        color = color or self.color
        vertices, faces = self.shape.to_vertices_and_faces(u=u)
        obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection)
        return [obj]


# src/compas_blender/artists/cylinderartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import Cylinder
from compas.artists import ShapeArtist
from .artist import BlenderArtist


class CylinderArtist(BlenderArtist, ShapeArtist):
    """Artist for drawing cylinder shapes in Blender.

    Parameters
    ----------
    cylinder : :class:`compas.geometry.Cylinder`
        A COMPAS cylinder.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.ShapeArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Plane, Circle, Cylinder
        from compas_blender.artists import CylinderArtist

        cylinder = Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0)

        artist = CylinderArtist(cylinder)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Plane, Circle, Cylinder
        from compas.artists import Artist

        cylinder = Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0)

        artist = Artist(cylinder)
        artist.draw()

    """

    def __init__(self,
                 cylinder: Cylinder,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(shape=cylinder, collection=collection or cylinder.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None, u: int = None) -> List[bpy.types.Object]:
        """Draw the cylinder associated with the artist.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the cylinder.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`~CylinderArtist.u`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]
            The objects created in Blender.

        """
        u = u or self.u
        color = color or self.color
        vertices, faces = self.shape.to_vertices_and_faces(u=u)
        obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection)
        return [obj]


# src/compas_blender/artists/frameartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

from compas.geometry import Frame

import compas_blender
from compas.artists import PrimitiveArtist
from .artist import BlenderArtist


class FrameArtist(BlenderArtist, PrimitiveArtist):
    """Artist for drawing frames in Blender.

    Parameters
    ----------
    frame: :class:`compas.geometry.Frame`
        A COMPAS frame.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    scale: float, optional
        Scale factor that controls the length of the axes.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.PrimitiveArtist`.

    Attributes
    ----------
    scale : float
        Scale factor that controls the length of the axes.
        Default is ``1.0``.
    color_origin : tuple of 3 int between 0 and 255
        Color for the point at the frame origin.
        Default is ``(0, 0, 0)``.
    color_xaxis : tuple of 3 int between 0 and 255
        Default is ``(255, 0, 0)``.
    color_yaxis : tuple of 3 int between 0 and 255
        Default is ``(0, 255, 0)``.
    color_zaxis : tuple of 3 int between 0 and 255
        Default is ``(0, 0, 255)``.

    """

    def __init__(self,
                 frame: Frame,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 scale: float = 1.0,
                 **kwargs: Any):

        super().__init__(primitive=frame, collection=collection or frame.name, **kwargs)

        self.scale = scale or 1.0
        self.color_origin = (0, 0, 0)
        self.color_xaxis = (255, 0, 0)
        self.color_yaxis = (0, 255, 0)
        self.color_zaxis = (0, 0, 255)

    def draw(self) -> List[bpy.types.Object]:
        """Draw the frame.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self.clear()
        objects = []
        objects += self.draw_origin()
        objects += self.draw_axes()
        return objects

    def draw_origin(self) -> List[bpy.types.Object]:
        """Draw the origin of the frame.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        points = [{
            'pos': self.primitive.point,
            'name': f"{self.primitive.name}.origin",
            'color': self.color_origin,
            'radius': 0.01
        }]
        return compas_blender.draw_points(points, self.collection)

    def draw_axes(self) -> List[bpy.types.Object]:
        """Draw the axes of the frame.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        origin = self.primitive.point
        X = self.primitive.point + self.primitive.xaxis.scaled(self.scale)
        Y = self.primitive.point + self.primitive.yaxis.scaled(self.scale)
        Z = self.primitive.point + self.primitive.zaxis.scaled(self.scale)
        lines = [
            {'start': origin, 'end': X, 'color': self.color_xaxis, 'name': f"{self.primitive.name}.xaxis"},
            {'start': origin, 'end': Y, 'color': self.color_yaxis, 'name': f"{self.primitive.name}.yaxis"},
            {'start': origin, 'end': Z, 'color': self.color_zaxis, 'name': f"{self.primitive.name}.zaxis"},
        ]
        return compas_blender.draw_lines(lines, self.collection)


# src/compas_blender/artists/lineartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.artists import PrimitiveArtist
from compas.geometry import Line
from compas_blender.artists import BlenderArtist


class LineArtist(BlenderArtist, PrimitiveArtist):
    """Artist for drawing lines in Blender.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`
        A COMPAS line.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.PrimitiveArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Line
        from compas_blender.artists import LineArtist

        line = Line([0, 0, 0], [1, 1, 1])

        artist = LineArtist(line)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Line
        from compas.artists import Artist

        line = Line([0, 0, 0], [1, 1, 1])

        artist = Artist(line)
        artist.draw()

    """

    def __init__(self,
                 line: Line,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any
                 ):
        super().__init__(primitive=line, collection=collection or line.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None, show_points: Optional[bool] = False) -> List[bpy.types.Object]:
        """Draw the line.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the box.
            The default color is :attr:`compas.artists.PrimitiveArtist.color`.
        show_points : bool, optional
            If True, show the start and end point in addition to the line.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        color = color or self.color
        start = self.primitive.start
        end = self.primitive.end
        objects = []
        if show_points:
            points = [
                {'pos': start, 'name': f"{self.primitive.name}.start", 'color': color, 'radius': 0.01},
                {'pos': end, 'name': f"{self.primitive.name}.end", 'color': color, 'radius': 0.01},
            ]
            objects += compas_blender.draw_points(points, collection=self.collection)
        lines = [
            {'start': start, 'end': end, 'color': color, 'name': f"{self.primitive.name}"},
        ]
        objects += compas_blender.draw_lines(lines, collection=self.collection)
        return objects


# src/compas_blender/artists/meshartist.py

from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

import bpy

from functools import partial

import compas_blender
from compas_blender.utilities import RGBColor
from compas.datastructures import Mesh
from compas.geometry import add_vectors
from compas.geometry import centroid_points
from compas.geometry import scale_vector

from compas.utilities import color_to_colordict
from compas.artists import MeshArtist
from .artist import BlenderArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=True)


class MeshArtist(BlenderArtist, MeshArtist):
    """Artist for drawing mesh data structures in Blender.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A COMPAS mesh.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    vertices : list[int], optional
        A list of vertex identifiers.
        Default is None, in which case all vertices are drawn.
    edges : list[tuple[int, int]], optional
        A list of edge keys (as uv pairs) identifying which edges to draw.
        The default is None, in which case all edges are drawn.
    faces : list[int], optional
        A list of face identifiers.
        The default is None, in which case all faces are drawn.
    vertexcolor : rgb-tuple or dict[int, rgb-tuple], optional
        The color specification for the vertices.
    edgecolor : rgb-tuple or dict[tuple[int, int], rgb-tuple], optional
        The color specification for the edges.
    facecolor : rgb-tuple or dict[int, rgb-tuple], optional
        The color specification for the faces.
    show_mesh : bool, optional
        If True, draw the mesh.
    show_vertices : bool, optional
        If True, draw the individual vertices.
    show_edges : bool, optional
        If True, draw the individual edges.
    show_faces : bool, optional
        If True, draw the individual faces.

    Attributes
    ----------
    vertexcollection : :blender:`bpy.types.Collection`
        The collection containing the vertices.
    edgecollection : :blender:`bpy.types.Collection`
        The collection containing the edges.
    facecollection : :blender:`bpy.types.Collection`
        The collection containing the faces.
    vertexlabelcollection : :blender:`bpy.types.Collection`
        The collection containing the vertex labels.
    edgelabelcollection : :blender:`bpy.types.Collection`
        The collection containing the edge labels.
    facelabelcollection : :blender:`bpy.types.Collection`
        The collection containing the face labels.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.datastructures import Mesh
        from compas_blender.artists import MeshArtist

        mesh = Mesh.from_meshgrid(10, 10)

        artist = MeshArtist(mesh)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.datastructures import Mesh
        from compas.artists import Artist

        mesh = Mesh.from_meshgrid(10, 10)

        artist = Artist(mesh)
        artist.draw()

    """

    def __init__(self,
                 mesh: Mesh,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 vertices: Optional[List[int]] = None,
                 edges: Optional[List[int]] = None,
                 faces: Optional[List[int]] = None,
                 vertexcolor: Optional[RGBColor] = None,
                 edgecolor: Optional[RGBColor] = None,
                 facecolor: Optional[RGBColor] = None,
                 show_mesh: bool = False,
                 show_vertices: bool = True,
                 show_edges: bool = True,
                 show_faces: bool = True,
                 **kwargs: Any):

        super().__init__(mesh=mesh, collection=collection or mesh.name, **kwargs)

        self.vertices = vertices
        self.edges = edges
        self.faces = faces
        self.vertex_color = vertexcolor
        self.edge_color = edgecolor
        self.face_color = facecolor
        self.show_mesh = show_mesh
        self.show_vertices = show_vertices
        self.show_edges = show_edges
        self.show_faces = show_faces

    @property
    def vertexcollection(self) -> bpy.types.Collection:
        if not self._vertexcollection:
            self._vertexcollection = compas_blender.create_collection('Vertices', parent=self.collection)
        return self._vertexcollection

    @property
    def edgecollection(self) -> bpy.types.Collection:
        if not self._edgecollection:
            self._edgecollection = compas_blender.create_collection('Edges', parent=self.collection)
        return self._edgecollection

    @property
    def facecollection(self) -> bpy.types.Collection:
        if not self._facecollection:
            self._facecollection = compas_blender.create_collection('Faces', parent=self.collection)
        return self._facecollection

    @property
    def vertexnormalcollection(self) -> bpy.types.Collection:
        if not self._vertexnormalcollection:
            self._vertexnormalcollection = compas_blender.create_collection('VertexNormals', parent=self.collection)
        return self._vertexnormalcollection

    @property
    def facenormalcollection(self) -> bpy.types.Collection:
        if not self._facenormalcollection:
            self._facenormalcollection = compas_blender.create_collection('FaceNormals', parent=self.collection)
        return self._facenormalcollection

    @property
    def vertexlabelcollection(self) -> bpy.types.Collection:
        if not self._vertexlabelcollection:
            self._vertexlabelcollection = compas_blender.create_collection('VertexLabels', parent=self.collection)
        return self._vertexlabelcollection

    @property
    def edgelabelcollection(self) -> bpy.types.Collection:
        if not self._edgelabelcollection:
            self._edgelabelcollection = compas_blender.create_collection('EdgeLabels', parent=self.collection)
        return self._edgelabelcollection

    @property
    def facelabelcollection(self) -> bpy.types.Collection:
        if not self._facelabelcollection:
            self._facelabelcollection = compas_blender.create_collection('FaceLabels', parent=self.collection)
        return self._facelabelcollection

    # ==========================================================================
    # clear
    # ==========================================================================

    def clear_vertices(self):
        """Clear the objects contained in the vertex collection (``self.vertexcollection``).

        Returns
        -------
        None

        """
        compas_blender.delete_objects(self.vertexcollection.objects)

    def clear_edges(self):
        """Clear the objects contained in the edge collection (``self.edgecollection``).

        Returns
        -------
        None

        """
        compas_blender.delete_objects(self.edgecollection.objects)

    def clear_faces(self):
        """Clear the objects contained in the face collection (``self.facecollection``).

        Returns
        -------
        None

        """
        compas_blender.delete_objects(self.facecollection.objects)

    # ==========================================================================
    # draw
    # ==========================================================================

    def draw(self,
             vertices: Optional[List[int]] = None,
             edges: Optional[List[Tuple[int, int]]] = None,
             faces: Optional[List[int]] = None,
             vertexcolor: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None,
             edgecolor: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None,
             facecolor: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
             ) -> None:
        """Draw the mesh using the chosen visualization settings.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertex keys identifying which vertices to draw.
            Default is None, in which case all vertices are drawn.
        edges : list[tuple[int, int]]
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        faces : list[int]
            A list of face keys identifying which faces to draw.
            The default is None, in which case all faces are drawn.
        vertexcolor : rgb-tuple or dict[int, rgb-tuple]
            The color specification for the vertices.
        edgecolor : rgb-tuple or dict[tuple[int, int], rgb-tuple]
            The color specification for the edges.
        facecolor : rgb-tuple or dict[int, rgb-tuple]
            The color specification for the faces.

        Returns
        -------
        None

        """
        self.clear()
        if self.show_mesh:
            self.draw_mesh()
        if self.show_vertices:
            self.draw_vertices(vertices=vertices, color=vertexcolor)
        if self.show_edges:
            self.draw_edges(edges=edges, color=edgecolor)
        if self.show_faces:
            self.draw_faces(faces=faces, color=facecolor)

    def draw_mesh(self) -> List[bpy.types.Object]:
        """Draw the mesh.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        vertices, faces = self.mesh.to_vertices_and_faces()
        obj = compas_blender.draw_mesh(vertices, faces, name=self.mesh.name, collection=self.collection)
        return [obj]

    def draw_vertices(self,
                      vertices: Optional[List[int]] = None,
                      color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                      ) -> List[bpy.types.Object]:
        """Draw a selection of vertices.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertex keys identifying which vertices to draw.
            Default is None, in which case all vertices are drawn.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification for the vertices.
            The default color of vertices is :attr:`default_vertexcolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self.vertex_color = color
        vertices = vertices or self.vertices
        points = []
        for vertex in vertices:
            points.append({
                'pos': self.vertex_xyz[vertex],
                'name': f"{self.mesh.name}.vertex.{vertex}",
                'color': self.vertex_color.get(vertex, self.default_vertexcolor),
                'radius': 0.01
            })
        return compas_blender.draw_points(points, self.vertexcollection)

    def draw_edges(self,
                   edges: Optional[List[Tuple[int, int]]] = None,
                   color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                   ) -> List[bpy.types.Object]:
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        color : rgb-tuple or dict[tuple[int, int], rgb-tuple], optional
            The color specification for the edges.
            The default color of edges is :attr:`default_edgecolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self.edge_color = color
        edges = edges or self.edges
        lines = []
        for edge in edges:
            lines.append({
                'start': self.vertex_xyz[edge[0]],
                'end': self.vertex_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': f"{self.mesh.name}.edge.{edge[0]}-{edge[1]}"
            })
        return compas_blender.draw_lines(lines, self.edgecollection)

    def draw_faces(self,
                   faces: Optional[List[int]] = None,
                   color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                   ) -> List[bpy.types.Object]:
        """Draw a selection of faces.

        Parameters
        ----------
        faces : list[int], optional
            A list of face keys identifying which faces to draw.
            The default is None, in which case all faces are drawn.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification for the faces.
            Th default color of faces is :attr:`default_facecolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self.face_color = color
        faces = faces or self.faces
        facets = []
        for face in faces:
            facets.append({
                'points': [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)],
                'name': f"{self.mesh.name}.face.{face}",
                'color': self.face_color.get(face, self.default_facecolor)
            })
        return compas_blender.draw_faces(facets, self.facecollection)

    # ==========================================================================
    # draw normals
    # ==========================================================================

    def draw_vertexnormals(self,
                           vertices: Optional[List[int]] = None,
                           color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = (0.0, 1.0, 0.0),
                           scale: float = 1.0) -> List[bpy.types.Object]:
        """Draw the normals at the vertices of the mesh.

        Parameters
        ----------
        vertices : list[int], optional
            A selection of vertex normals to draw.
            Default is to draw all vertex normals.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification of the normal vectors.
        scale : float, optional
            Scale factor for the vertex normals.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        vertices = vertices or self.vertices
        vertex_color = colordict(color, vertices, default=color)
        lines = []
        for vertex in vertices:
            a = self.vertex_xyz[vertex]
            n = self.mesh.vertex_normal(vertex)
            b = add_vectors(a, scale_vector(n, scale))
            lines.append({
                'start': a,
                'end': b,
                'color': vertex_color[vertex],
                'name': "{}.vertexnormal.{}".format(self.mesh.name, vertex)
            })
        return compas_blender.draw_lines(lines, collection=self.vertexnormalcollection)

    def draw_facenormals(self,
                         faces: Optional[List[List[int]]] = None,
                         color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = (0.0, 1.0, 1.0),
                         scale: float = 1.0) -> List[bpy.types.Object]:
        """Draw the normals of the faces.

        Parameters
        ----------
        faces : list[int], optional
            A selection of face normals to draw.
            Default is to draw all face normals.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification of the normal vectors.
        scale : float, optional
            Scale factor for the face normals.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        faces = faces or self.faces
        face_color = colordict(color, faces, default=color)
        lines = []
        for face in faces:
            a = centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)])
            n = self.mesh.face_normal(face)
            b = add_vectors(a, scale_vector(n, scale))
            lines.append({
                'start': a,
                'end': b,
                'name': "{}.facenormal.{}".format(self.mesh.name, face),
                'color': face_color[face]
            })
        return compas_blender.draw_lines(lines, collection=self.facenormalcollection)

    # ==========================================================================
    # draw labels
    # ==========================================================================

    def draw_vertexlabels(self,
                          text: Optional[Dict[int, str]] = None,
                          color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                          ) -> List[bpy.types.Object]:
        """Draw labels for a selection vertices.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of vertex labels as vertex-text pairs.
            The default value is None, in which case every vertex will be labeled with its identifier.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification of the labels.
            The default color is the same as the default vertex color, :attr:`default_vertexcolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        if not text or text == 'key':
            vertex_text = {vertex: str(vertex) for vertex in self.vertices}
        elif text == 'index':
            vertex_text = {vertex: str(index) for index, vertex in enumerate(self.vertices)}
        elif isinstance(text, dict):
            vertex_text = text
        else:
            raise NotImplementedError
        vertex_color = colordict(color, vertex_text, default=self.default_vertexcolor)
        labels = []
        for vertex in vertex_text:
            labels.append({
                'pos': self.vertex_xyz[vertex],
                'name': "{}.vertexlabel.{}".format(self.mesh.name, vertex),
                'text': vertex_text[vertex],
                'color': vertex_color[vertex]
            })
        return compas_blender.draw_texts(labels, collection=self.vertexlabelcollection)

    def draw_edgelabels(self,
                        text: Optional[Dict[Tuple[int, int], str]] = None,
                        color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                        ) -> List[bpy.types.Object]:
        """Draw labels for a selection of edges.

        Parameters
        ----------
        text : dict[tuple[int, int], str], optional
            A dictionary of edge labels as edge-text pairs.
            The default value is None, in which case every edge will be labeled with its identifier.
        color : rgb-tuple or dict[tuple[int, int], rgb-tuple], optional
            The color specification of the labels.
            The default color is the same as the default color for edges, :attr:`default_edgecolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        if text is None:
            edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.edges}
        elif isinstance(text, dict):
            edge_text = text
        else:
            raise NotImplementedError
        edge_color = colordict(color, edge_text, default=self.default_edgecolor)
        labels = []
        for edge in edge_text:
            labels.append({
                'pos': centroid_points([self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]]),
                'name': "{}.edgelabel.{}-{}".format(self.mesh.name, *edge),
                'text': edge_text[edge]
            })
        return compas_blender.draw_texts(labels, collection=self.edgelabelcollection, color=edge_color)

    def draw_facelabels(self,
                        text: Optional[Dict[int, str]] = None,
                        color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                        ) -> List[bpy.types.Object]:
        """Draw labels for a selection of faces.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of face labels as face-text pairs.
            The default value is None, in which case every face will be labeled with its identifier.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification of the labels.
            The default color is the same as the default face color: :attr:`default_facecolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        if not text or text == 'key':
            face_text = {face: str(face) for face in self.faces}
        elif text == 'index':
            face_text = {face: str(index) for index, face in enumerate(self.faces)}
        elif isinstance(text, dict):
            face_text = text
        else:
            raise NotImplementedError
        face_color = color or self.default_facecolor
        labels = []
        for face in face_text:
            labels.append({
                'pos': centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]),
                'name': "{}.facelabel.{}".format(self.mesh.name, face),
                'text': face_text[face]
            })
        return compas_blender.draw_texts(labels, collection=self.collection, color=face_color)


# src/compas_blender/artists/networkartist.py

from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

import bpy
from functools import partial

import compas_blender
from compas_blender.utilities import RGBColor
from compas.datastructures import Network
from compas.geometry import centroid_points
from compas.utilities import color_to_colordict
from compas.artists import NetworkArtist
from .artist import BlenderArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=True)


class NetworkArtist(BlenderArtist, NetworkArtist):
    """Artist for drawing network data structures in Blender.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A COMPAS network.
    collection : str or :blender:`bpy.types.Collection`
        The name of the collection the object belongs to.
    nodes : list[hashable], optional
        A list of node identifiers.
        Default is None, in which case all nodes are drawn.
    edges : list[tuple[hashable, hashable]], optional
        A list of edge keys (as uv pairs) identifying which edges to draw.
        The default is None, in which case all edges are drawn.
    nodecolor : rgb-tuple or dict[hashable, rgb-tuple], optional
        The color specification for the nodes.
    edgecolor : rgb-tuple or dict[tuple[hashable, hashable]], rgb-tuple], optional
        The color specification for the edges.
    show_nodes : bool, optional
        If True, draw the nodes of the network.
    show_edges : bool, optional
        If True, draw the edges of the network.

    Attributes
    ----------
    nodecollection : :blender:`bpy.types.Collection`
        The collection containing the nodes.
    edgecollection : :blender:`bpy.types.Collection`
        The collection containing the edges.
    nodelabelcollection : :blender:`bpy.types.Collection`
        The collection containing the node labels.
    edgelabelcollection : :blender:`bpy.types.Collection`
        The collection containing the edge labels.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        import compas
        from compas.datastructures import Network
        from compas_blender.artists import NetworkArtist

        network = Network.from_obj(compas.get('lines.obj'))

        artist = NetworkArtist(network)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        import compas
        from compas.datastructures import Network
        from compas.artists import Artist

        network = Network.from_obj(compas.get('lines.obj'))

        artist = Artist(network)
        artist.draw()

    """

    def __init__(self,
                 network: Network,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 nodes: Optional[List[int]] = None,
                 edges: Optional[List[int]] = None,
                 nodecolor: RGBColor = (1, 1, 1),
                 edgecolor: RGBColor = (0, 0, 0),
                 show_nodes: bool = True,
                 show_edges: bool = True,
                 **kwargs: Any):

        super().__init__(network=network, collection=collection or network.name, **kwargs)

        self.nodes = nodes
        self.edges = edges
        self.node_color = nodecolor
        self.edge_color = edgecolor
        self.show_nodes = show_nodes
        self.show_edges = show_edges

    @property
    def nodecollection(self) -> bpy.types.Collection:
        if not self._nodecollection:
            self._nodecollection = compas_blender.create_collection('Nodes', parent=self.collection)
        return self._nodecollection

    @property
    def edgecollection(self) -> bpy.types.Collection:
        if not self._edgecollection:
            self._edgecollection = compas_blender.create_collection('Edges', parent=self.collection)
        return self._edgecollection

    @property
    def nodelabelcollection(self) -> bpy.types.Collection:
        if not self._nodelabelcollection:
            self._nodelabelcollection = compas_blender.create_collection('NodeLabels', parent=self.collection)
        return self._nodelabelcollection

    @property
    def edgelabelcollection(self) -> bpy.types.Collection:
        if not self._edgelabelcollection:
            self._edgelabelcollection = compas_blender.create_collection('EdgeLabels', parent=self.collection)
        return self._edgelabelcollection

    # ==========================================================================
    # clear
    # ==========================================================================

    def clear_nodes(self):
        """Clear all objects contained in the node collection.

        Returns
        -------
        None

        """
        compas_blender.delete_objects(self.nodecollection.objects)

    def clear_edges(self):
        """Clear all objects contained in the edge collection.

        Returns
        -------
        None

        """
        compas_blender.delete_objects(self.edgecollection.objects)

    def clear_nodelabels(self):
        """Clear all objects contained in the nodelabel collection.

        Returns
        -------
        None

        """
        compas_blender.delete_objects(self.nodelabelcollection.objects)

    def clear_edgelabels(self):
        """Clear all objects contained in the edgelabel collection.

        Returns
        -------
        None

        """
        compas_blender.delete_objects(self.edgelabelcollection.objects)

    # ==========================================================================
    # draw
    # ==========================================================================

    def draw(self,
             nodes: Optional[List[int]] = None,
             edges: Optional[Tuple[int, int]] = None,
             nodecolor: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None,
             edgecolor: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
             ) -> None:
        """Draw the network.

        Parameters
        ----------
        nodes : list[hashable], optional
            A list of node identifiers.
            Default is None, in which case all nodes are drawn.
        edges : list[tuple[hashable, hashable]], optional
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        nodecolor : rgb-tuple or dict[hashable, rgb-tuple], optional
            The color specification for the nodes.
        edgecolor : rgb-tuple or dict[tuple[hashable, hashable], rgb-tuple], optional
            The color specification for the edges.

        Returns
        -------
        None

        """
        self.clear()
        if self.show_nodes:
            self.draw_nodes(nodes=nodes, color=nodecolor)
        if self.show_edges:
            self.draw_edges(edges=edges, color=edgecolor)

    def draw_nodes(self,
                   nodes: Optional[List[int]] = None,
                   color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                   ) -> List[bpy.types.Object]:
        """Draw a selection of nodes.

        Parameters
        ----------
        nodes : list[hashable], optional
            A list of node identifiers.
            Default is None, in which case all nodes are drawn.
        color : rgb-tuple or dict[hashable, rgb-tuple], optional
            The color specification for the nodes.
            The default color of nodes is :attr:`default_nodecolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self.node_color = color
        nodes = nodes or self.nodes
        points = []
        for node in nodes:
            points.append({
                'pos': self.node_xyz[node],
                'name': f"{self.network.name}.node.{node}",
                'color': self.node_color.get(node, self.default_nodecolor),
                'radius': 0.05
            })
        return compas_blender.draw_points(points, self.nodecollection)

    def draw_edges(self,
                   edges: Optional[Tuple[int, int]] = None,
                   color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                   ) -> List[bpy.types.Object]:
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[hashable, hashable]], optional
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        color : rgb-tuple or dict[tuple[hashable, hashable], rgb-tuple], optional
            The color specification for the edges.
            The default color of edges is :attr:`default_edgecolor`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self.edge_color = color
        edges = edges or self.edges
        lines = []
        for edge in edges:
            lines.append({
                'start': self.node_xyz[edge[0]],
                'end': self.node_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': f"{self.network.name}.edge.{edge[0]}-{edge[1]}",
                'width': 0.02
            })
        return compas_blender.draw_lines(lines, self.edgecollection)

    def draw_nodelabels(self,
                        text: Optional[Dict[int, str]] = None,
                        color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                        ) -> List[bpy.types.Object]:
        """Draw labels for a selection nodes.

        Parameters
        ----------
        text : dict[hashable, str], optional
            A dictionary of vertex labels as vertex-text pairs.
            The default value is None, in which case every vertex will be labeled with its key.
        color : rgb-tuple or dict[hashable, rgb-tuple], optional
            The color specification of the labels.
            The default color is the same as the default vertex color.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        if not text or text == 'key':
            node_text = {vertex: str(vertex) for vertex in self.nodes}
        elif text == 'index':
            node_text = {vertex: str(index) for index, vertex in enumerate(self._nodes)}
        elif isinstance(text, dict):
            node_text = text
        else:
            raise NotImplementedError
        node_color = colordict(color, node_text, default=self.default_nodecolor)
        labels = []
        for node in node_text:
            labels.append({
                'pos': self.node_xyz[node],
                'name': "{}.nodelabel.{}".format(self.network.name, node),
                'text': node_text[node],
                'color': node_color[node]
            })
        return compas_blender.draw_texts(labels, collection=self.nodelabelcollection)

    def draw_edgelabels(self,
                        text: Optional[Dict[Tuple[int, int], str]] = None,
                        color: Optional[Union[str, RGBColor, List[RGBColor], Dict[int, RGBColor]]] = None
                        ) -> List[bpy.types.Object]:
        """Draw labels for a selection of edges.

        Parameters
        ----------
        text : dict[tuple[hashable, hashable], str], optional
            A dictionary of edge labels as edge-text pairs.
            The default value is None, in which case every edge will be labeled with its key.
        color : rgb-tuple or dict[tuple[hashable, hashable], rgb-tuple], optional
            The color specification of the labels.
            The default color is the same as the default color for edges.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        if text is None:
            edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.edges}
        elif isinstance(text, dict):
            edge_text = text
        else:
            raise NotImplementedError
        edge_color = colordict(color, edge_text, default=self.default_edgecolor)
        labels = []
        for edge in edge_text:
            labels.append({
                'pos': centroid_points([self.node_xyz[edge[0]], self.node_xyz[edge[1]]]),
                'name': "{}.edgelabel.{}-{}".format(self.network.name, *edge),
                'text': edge_text[edge]
            })
        return compas_blender.draw_texts(labels, collection=self.edgelabelcollection, color=edge_color)


# src/compas_blender/artists/pointartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.artists import PrimitiveArtist
from compas.geometry import Point
from compas_blender.artists import BlenderArtist


class PointArtist(BlenderArtist, PrimitiveArtist):
    """Artist for drawing points in Blender.

    Parameters
    ----------
    point : :class:`compas.geometry.Point`
        A COMPAS point.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.PrimitiveArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Point
        from compas_blender.artists import PointArtist

        point = Point(0, 0, 0)

        artist = PointArtist(point)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Point
        from compas.artists import Artist

        point = Point(0, 0, 0)

        artist = Artist(point)
        artist.draw()

    """

    def __init__(self,
                 point: Point,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):
        super().__init__(primitive=point, collection=collection or point.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None) -> List[bpy.types.Object]:
        """Draw the point.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            Color of the point object.
            The default color is :attr:`compas.artists.PrimitiveArtist.color`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        color = color or self.color
        points = [{
            'pos': self.primitive,
            'name': f"{self.primitive.name}",
            'color': color,
            'radius': 0.01
        }]
        objects = compas_blender.draw_points(points, self.collection)
        return objects


# src/compas_blender/artists/polygonartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.artists import PrimitiveArtist
from compas.geometry import Polygon
from compas_blender.artists import BlenderArtist


class PolygonArtist(BlenderArtist, PrimitiveArtist):
    """Artist for drawing polygons in Blender.

    Parameters
    ----------
    polygon : :class:`compas.geometry.Polygon`
        A COMPAS polygon.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.PrimitiveArtist`.

    Examples
    --------
    .. code-block:: python

        from compas.geometry import Polygon
        from compas_blender.artists import PolygonArtist

        polygon = Polygon.from_sides_and_radius_xy(5, 1)

        artist = PolygonArtist(polygon)
        artist.draw()

    .. code-block:: python

        from compas.geometry import Polygon
        from compas.artists import Artist

        polygon = Polygon.from_sides_and_radius_xy(5, 1)

        artist = Artist(polygon)
        artist.draw()

    """

    def __init__(self,
                 polygon: Polygon,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):
        super().__init__(primitive=polygon, collection=collection or polygon.name, **kwargs)

    def draw(self,
             color: Optional[RGBColor] = None,
             show_points: bool = False,
             show_edges: bool = False,
             show_face: bool = True) -> List[bpy.types.Object]:
        """Draw the polygon.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the polygon.
            The default color is :attr:`compas.artists.PrimitiveArtist.color`.
        show_points : bool, optional
            If True, draw the corner points of the polygon.
        show_edges : bool, optional
            If True, draw the edges of the polygon.
        show_face : bool, optional
            If True, draw the face of the polygon.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        color = color or self.color
        objects = []
        if show_points:
            points = [{'pos': point, 'color': color, 'name': self.primitive.name, 'radius': 0.01} for point in self.primitive.points]
            objects += compas_blender.draw_points(points, collection=self.collection)
        if show_edges:
            lines = [{'start': a, 'end': b, 'color': color, 'name': self.primitive.name} for a, b in self.primitive.lines]
            objects += compas_blender.draw_lines(lines, collection=self.collection)
        if show_face:
            polygons = [{'points': self.primitive.points, 'color': color, 'name': self.primitive.name}]
            objects += compas_blender.draw_faces(polygons, collection=self.collection)
        return objects


# src/compas_blender/artists/polyhedronartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy
import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import Polyhedron
from compas.artists import ShapeArtist
from .artist import BlenderArtist


class PolyhedronArtist(BlenderArtist, ShapeArtist):
    """Artist for drawing polyhedron shapes in Blender.

    Parameters
    ----------
    polyhedron : :class:`compas.geometry.Polyhedron`
        A COMPAS polyhedron.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.ShapeArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Polyhedron
        from compas_blender.artists import PolyhedronArtist

        polyhedron = Polyhedron.from_platonicsolid(12)

        artist = PolyhedronArtist(polyhedron)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Polyhedron
        from compas.artists import Artist

        polyhedron = Polyhedron.from_platonicsolid(12)

        artist = Artist(polyhedron)
        artist.draw()

    """

    def __init__(self,
                 polyhedron: Polyhedron,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(shape=polyhedron, collection=collection or polyhedron.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None) -> List[bpy.types.Object]:
        """Draw the polyhedron associated with the artist.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the polyhedron.
            The default color is :attr:`compas.artists.ShapeArtist.color`.

        Returns
        -------
        list[:blender:`bpy.types.Object`]
            The objects created in Blender.

        """
        color = color or self.color
        vertices, faces = self.shape.to_vertices_and_faces()
        obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection)
        return [obj]


# src/compas_blender/artists/polylineartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.artists import PrimitiveArtist
from compas.geometry import Polyline
from compas_blender.artists import BlenderArtist


class PolylineArtist(BlenderArtist, PrimitiveArtist):
    """Artist for drawing polylines in Blender.

    Parameters
    ----------
    polyline : :class:`compas.geometry.Polyline`
        A COMPAS polyline.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.PrimitiveArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Polyline
        from compas_blender.artists import PolylineArtist

        polyline = Polyline([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]])

        artist = PolylineArtist(polyline)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Polyline
        from compas.artists import Artist

        polyline = Polyline([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]])

        artist = Artist(polyline)
        artist.draw()

    """

    def __init__(self,
                 polyline: Polyline,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any
                 ):
        super().__init__(primitive=polyline, collection=collection or polyline.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None, show_points: Optional[bool] = False) -> List[bpy.types.Object]:
        """Draw the line.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the polyline.
            The default color is :attr:`compas.artists.PrimitiveArtist.color`.
        show_points : bool, optional
            If True, draw the corner points of the polyline.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        color = color or self.color
        _points = map(list, self.primitive.points)

        lines = [
            {'start': start, 'end': end, 'color': self.color, 'name': f"{self.primitive.name}"}
            for start, end in self.primitive.lines
        ]
        objects = compas_blender.draw_lines(lines, collection=self.collection)

        if show_points:
            points = [
                {'pos': point, 'name': f"{self.primitive.name}.point", 'color': color, 'radius': 0.01}
                for point in _points
            ]
            objects += compas_blender.draw_points(points, collection=self.collection)
        return objects


# src/compas_blender/artists/robotmodelartist.py

from typing import Union
from typing import Tuple
from typing import Optional
from typing import Any
from typing import List

import bpy
import mathutils

import compas_blender
from compas_blender.utilities import RGBColor
from compas.datastructures import Mesh
from compas.geometry import Transformation
from compas.robots import RobotModel
from compas.artists import RobotModelArtist
from .artist import BlenderArtist


class RobotModelArtist(BlenderArtist, RobotModelArtist):
    """Artist for drawing robot models in Blender.

    Parameters
    ----------
    model : :class:`compas.robots.RobotModel`
        Robot model.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.RobotModelArtist`.

    """

    def __init__(self,
                 model: RobotModel,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):
        super().__init__(model=model, collection=collection or model.name, **kwargs)

    # this method should not be here
    # it has nothing to do with the current object
    def transform(self, native_mesh: bpy.types.Object, transformation: Transformation) -> None:
        """Transform the mesh of a robot model.

        Parameters
        ----------
        native_mesh : bpy.types.Object
            A mesh scene object.
        transformation : :class:`compas.geometry.Transformation`
            A transformation matrix.

        Returns
        -------
        None

        """
        native_mesh.matrix_world = mathutils.Matrix(transformation.matrix) @ native_mesh.matrix_world

    # again
    # doesn't make sense to me that there is no reference to self (except for the collection)
    # suggests that this method shouldn't be here
    def create_geometry(self,
                        geometry: Mesh,
                        name: str = None,
                        color: Union[RGBColor, Tuple[int, int, int, int], Tuple[float, float, float, float]] = None
                        ) -> bpy.types.Object:
        """Create the scene objecy representing the robot geometry.

        Parameters
        ----------
        geometry : :class:`compas.datastructures.Mesh`
            The geometry representing the robot.
        name : str, optional
            A name for the scene object.
        color : tuple[int, int, int] or tuple[float, float, float], optional
            The color of the object.

        Returns
        -------
        bpy.types.Object

        """
        # Imported colors take priority over a the parameter color
        if 'mesh_color.diffuse' in geometry.attributes:
            color = geometry.attributes['mesh_color.diffuse']

        # If we have a color, we'll discard alpha because draw_mesh is hard coded for a=1
        if color:
            color = color[:3]
        else:
            color = (1., 1., 1.)

        v, f = geometry.to_vertices_and_faces()
        native_mesh = compas_blender.draw_mesh(
            vertices=v, faces=f, name=name, color=color, centroid=False, collection=self.collection
        )
        native_mesh.hide_set(True)
        return native_mesh

    def _ensure_geometry(self):
        if len(self.collection.objects) == 0:
            self.create()

    def draw(self) -> List[bpy.types.Object]:
        """Draw the robot model.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self._ensure_geometry()
        return self.draw_visual()

    def draw_visual(self) -> List[bpy.types.Object]:
        """Draw the robot model.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self._ensure_geometry()
        visuals = super(RobotModelArtist, self).draw_visual()
        for visual in visuals:
            visual.hide_set(False)
        return visuals

    def draw_collision(self) -> List[bpy.types.Object]:
        """Draw the collision mesh of the robot model.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self._ensure_geometry()
        collisions = super(RobotModelArtist, self).draw_collision()
        for collision in collisions:
            collision.hide_set(False)
        return collisions

    def draw_attached_meshes(self) -> List[bpy.types.Object]:
        """Draw the meshes attached to the robot model, if any.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        self._ensure_geometry()
        meshes = super(RobotModelArtist, self).draw_attached_meshes()
        for mesh in meshes:
            mesh.hide_set(False)
        return meshes


# src/compas_blender/artists/sphereartist.py

from typing import Optional
from typing import Any
from typing import List
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import Sphere
from compas.artists import ShapeArtist
from .artist import BlenderArtist


class SphereArtist(BlenderArtist, ShapeArtist):
    """Artist for drawing sphere shapes in Blender.

    Parameters
    ----------
    sphere : :class:`compas.geometry.Sphere`
        A COMPAS sphere.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.ShapeArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Sphere
        from compas_blender.artists import SphereArtist

        sphere = Sphere([0, 0, 0], 1)

        artist = SphereArtist(sphere)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Sphere
        from compas.artists import Artist

        sphere = Sphere([0, 0, 0], 1)

        artist = Artist(sphere)
        artist.draw()

    """

    def __init__(self,
                 sphere: Sphere,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(shape=sphere, collection=collection or sphere.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None, u: int = None, v: int = None) -> List[bpy.types.Object]:
        """Draw the sphere associated with the artist.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the sphere.
            The default color is :attr:`compas.artists.ShapeArtist.color`.
        u : int, optional
            Number of faces in the "u" direction.
            Default is ``SphereArtist.u``.
        v : int, optional
            Number of faces in the "v" direction.
            Default is ``SphereArtist.v``.

        Returns
        -------
        list
            The objects created in Blender.
        """
        u = u or self.u
        v = v or self.v
        color = color or self.color
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection)
        return [obj]


# src/compas_blender/artists/torusartist.py

from typing import Optional
from typing import Any
from typing import List
from typing import Union

import bpy

import compas_blender
from compas_blender.utilities import RGBColor
from compas.geometry import Torus
from compas.artists import ShapeArtist
from .artist import BlenderArtist


class TorusArtist(BlenderArtist, ShapeArtist):
    """Artist for drawing torus shapes in Blender.

    Parameters
    ----------
    torus : :class:`compas.geometry.Torus`
        A COMPAS torus.
    collection: str or :blender:`bpy.types.Collection`
        The name of the collection the object belongs to.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.ShapeArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Plane, Torus
        from compas_blender.artists import TorusArtist

        torus = Torus(Plane([0, 0, 0], [0, 0, 1]), 1.0, 0.3)

        artist = TorusArtist(torus)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Plane, Torus
        from compas.artists import Artist

        torus = Torus(Plane([0, 0, 0], [0, 0, 1]), 1.0, 0.3)

        artist = Artist(torus)
        artist.draw()

    """

    def __init__(self,
                 torus: Torus,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(shape=torus, collection=collection or torus.name, **kwargs)

    def draw(self, color: Optional[RGBColor] = None, u: Optional[int] = None, v: Optional[int] = None) -> List[bpy.types.Object]:
        """Draw the torus associated with the artist.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the torus.
            The default color is :attr:`compas.artists.ShapeArtist.color`.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`TorusArtist.u`.
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`TorusArtist.v`.

        Returns
        -------
        list
            The objects created in Blender.
        """
        u = u or self.u
        v = v or self.v
        color = color or self.color
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection)
        return [obj]


# src/compas_blender/artists/vectorartist.py

from typing import Any
from typing import List
from typing import Optional
from typing import Union

import bpy

import compas_blender
from compas_blender.artists import BlenderArtist
from compas_blender.utilities import RGBColor
from compas.artists import PrimitiveArtist
from compas.geometry import Point
from compas.geometry import Vector


class VectorArtist(BlenderArtist, PrimitiveArtist):
    """Artist for drawing vectors in Blender.

    Parameters
    ----------
    primitive : :class:`compas.geometry.Vector`
        A COMPAS vector.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.PrimitiveArtist`.

    Examples
    --------
    Use the Blender artist explicitly.

    .. code-block:: python

        from compas.geometry import Vector
        from compas_blender.artists import VectorArtist

        vector = Vector(1, 1, 1)

        artist = VectorArtist(vector)
        artist.draw()

    Or, use the artist through the plugin mechanism.

    .. code-block:: python

        from compas.geometry import Vector
        from compas.artists import Artist

        vector = Vector(1, 1, 1)

        artist = Artist(vector)
        artist.draw()

    """

    def __init__(self,
                 vector: Vector,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):
        super().__init__(primitive=vector, collection=collection or vector.name, **kwargs)

    def draw(self,
             color: Optional[RGBColor] = None,
             point: Optional[Point] = None,
             show_point: Optional[bool] = False) -> List[bpy.types.Object]:
        """Draw the vector.

        Parameters
        ----------
        color : tuple[float, float, float] or tuple[int, int, int], optional
            The RGB color of the vector.
            The default color is :attr:`compas.artists.PrimitiveArtist.color`.
        point : [float, float, float] or :class:`compas.geometry.Point`, optional
            Point of application of the vector.
            Default is ``Point(0, 0, 0)``.
        show_point : bool, optional
            If True, draw the point of application of the vector.

        Returns
        -------
        list[:blender:`bpy.types.Object`]

        """
        start = point or (0., 0., 0.)
        end = tuple(map(sum, zip(start, self.primitive)))
        color = color or self. color
        lines = [
            {'start': start, 'end': end, 'color': color, 'name': f"{self.primitive.name}"},
        ]
        objects = compas_blender.draw_lines(lines, self.collection)
        if show_point:
            points = [{
                'pos': start,
                'name': f"{self.primitive.name}.origin",
                'color': (1.0, 1.0, 1.0),
                'radius': 0.01,
            }]
            objects += compas_blender.draw_points(points, self.collection)
        return objects


# src/compas_blender/artists/volmeshartist.py

from typing import Optional
from typing import Union
from typing import Any

import bpy

from compas.artists import MeshArtist
from .artist import BlenderArtist


class VolMeshArtist(BlenderArtist, MeshArtist):
    """An artist for drawing volumetric mesh data structures in Blender.

    Parameters
    ----------
    volmesh : :class:`compas.datastructures.VolMesh`
        The volmesh data structure.
    collection : str or :blender:`bpy.types.Collection`
        The Blender scene collection the object(s) created by this artist belong to.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info,
        see :class:`compas_blender.artists.BlenderArtist` and :class:`compas.artists.MeshArtist`.

    """

    def __init__(self, volmesh,
                 collection: Optional[Union[str, bpy.types.Collection]] = None,
                 **kwargs: Any):

        super().__init__(volmesh=volmesh, collection=collection or volmesh.name, **kwargs)


# src/compas_blender/conversions/__init__.py

"""
********************************************************************************
conversions
********************************************************************************

.. currentmodule:: compas_blender.conversions

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    BlenderGeometry
    BlenderCurve
    BlenderMesh

"""
from ._geometry import BlenderGeometry
from .curve import BlenderCurve
from .mesh import BlenderMesh

__all__ = [
    'BlenderGeometry',
    'BlenderCurve',
    'BlenderMesh'
]


# src/compas_blender/conversions/_geometry.py

import bpy
from compas.geometry import Point


class BlenderGeometry:
    """Base class for Blender Geometry and Object wrappers.

    Blender scene objects have an underlying data block that represents the actual geometry of the object.
    Multiple objects can share the same data block.

    Every object adds a "location", "rotation", and "scale" to the data block to place it in the scene.
    To change an individual object without changing the underlying data block, change its location, rotation or scale.
    Change a data block directly to change all the connected objects.
    """

    def __init__(self):
        self._object = None
        self._geometry = None

    @property
    def object(self):
        """:blender:`bpy.types.Object` - The Blender scene object."""
        return self._object

    @object.setter
    def object(self, obj):
        raise NotImplementedError

    @property
    def geometry(self):
        """The data block."""
        return self._geometry

    @geometry.setter
    def geometry(self, data):
        raise NotImplementedError

    @property
    def type(self):
        """:obj:`str` - The type of Blender object."""
        if self.object:
            return self.object.type

    @property
    def name(self):
        """:obj:`str` - The name of the Blender object."""
        if self.object:
            return self.object.name

    @name.setter
    def name(self, value):
        if self.object:
            self.object.name = value

    @property
    def location(self):
        """:class:`compas.geometry.Point` - The location of the Blender object."""
        return Point(* self.object.location)

    @location.setter
    def location(self, location):
        self.object.location = list(location)

    @classmethod
    def from_object(cls, obj):
        """Construct a Blender object wrapper from an existing Blender object.

        Parameters
        ----------
        obj : :blender:`bpy.types.Object`
            The Blender object.

        Returns
        -------
        :class:`compas_blender.conversions.BlenderGeometry`
            The Blender object wrapper.

        Raises
        ------
        :class:`ConversionError`
            If the geometry of the Blender scene object cannot be converted to the geometry type of the wrapper.
        """
        wrapper = cls()
        wrapper.object = obj
        return wrapper

    @classmethod
    def from_geometry(cls, geometry):
        """Construct a Blender object wrapper from an existing Blender data block.

        Parameters
        ----------
        geometry : Blender data block
            The Blender data block.

        Returns
        -------
        :class:`compas_blender.conversions.BlenderGeometry`
            The Blender object wrapper.

        Raises
        ------
        :class:`ConversionError`
            If the geometry of the Blender data block cannot be converted to the geometry type of the wrapper.
        """
        wrapper = cls()
        wrapper.geometry = geometry
        return wrapper

    @classmethod
    def from_name(cls, name):
        """Construct a Blender object wrapper from an existing Blender object.

        Parameters
        ----------
        name : :obj:`str`
            The name of the Blender scene object.

        Returns
        -------
        :class:`compas_blender.conversions.BlenderGeometry`
            The Blender object wrapper.

        Raises
        ------
        :class:`ConversionError`
            If the geometry of the Blender data block cannot be converted to the geometry type of the wrapper.
        """
        wrapper = cls()
        wrapper.object = bpy.data.objects[name]
        return wrapper

    def to_compas(self):
        raise NotImplementedError

    def transform(self, T):
        """Transform the Blender object.

        Parameters
        ----------
        T : :class:`compas.geometry.Transformation`
            The transformation matrix.

        Returns
        -------
        None
            The Blender object is transformed in place.
        """
        raise NotImplementedError


# src/compas_blender/conversions/curve.py

# from mathutils.geometry import interpolate_bezier

# from compas.geometry import add_vectors
from ._geometry import BlenderGeometry


class BlenderCurve(BlenderGeometry):
    """Wrapper for Blender curves.

    Examples
    --------
    .. code-block:: python

        pass
    """

    @property
    def geometry(self):
        """:blender:`bpy.types.Curve` - The curve geometry data block."""
        return self._geometry

    @geometry.setter
    def geometry(self, datablock):
        self._object = None
        self._geometry = datablock

    def to_compas(self):
        """Convert the curve to a COMPAS curve.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`
        """
        from compas.geometry import NurbsCurve
        curve = NurbsCurve()
        curve.rhino_curve = self.geometry
        return curve

    # def control_points(self):
    #     return self.geometry.splines[0].bezier_points

    # def control_point_coordinates(self):
    #     points = self.control_points()
    #     middle = [list(i.co) for i in points]
    #     left = [list(i.handle_left) for i in points]
    #     right = [list(i.handle_right) for i in points]
    #     return middle, left, right

    # def divide(self, number_of_segments):
    #     m, l, r = self.control_point_coordinates()
    #     points = [list(i) for i in interpolate_bezier(m[0], r[0], l[1], m[1], number_of_segments + 1)]
    #     return [add_vectors(self.location, point) for point in points]


# src/compas_blender/conversions/mesh.py

import bpy
import bmesh

from compas.datastructures import Mesh
from compas.geometry import Point

from ._geometry import BlenderGeometry


class BlenderMesh(BlenderGeometry):
    """Wrapper for Blender meshes.

    Examples
    --------
    .. code-block:: python

        pass
    """

    @property
    def object(self):
        """:blender:`bpy.types.Object` - The Blender scene object."""
        return self._object

    @object.setter
    def object(self, obj):
        mesh = bpy.data.meshes.new_from_object(obj)
        self._object = obj
        self._geometry = mesh

    @property
    def geometry(self):
        """:blender:`bpy.types.Mesh` - The mesh data block."""
        return self._geometry

    @geometry.setter
    def geometry(self, data):
        self._object = None
        self._geometry = data

    @property
    def bmesh(self):
        """:blender:`bpy.types.BMesh` - The mesh data structure."""
        return bmesh.from_edit_mesh(self.mesh)

    @property
    def location(self):
        """:class:`compas.geometry.Point` - The location of the object in the scene."""
        return Point(self.geometry.location)

    @property
    def vertices(self):
        """List[:class:`compas.geometry.Point`] - The mesh vertex locations."""
        point = self.location
        return [point + list(vertex.co) for vertex in self.geometry.vertices]

    @property
    def faces(self):
        """List[List[:obj:`int`]] - The mesh face vertices."""
        return [list(face.vertices) for face in self.geometry.polygons]

    @classmethod
    def from_bmesh(cls, bm, name=None, free=True):
        """Construct a Blender mesh wrappper from a BMesh.

        Parameters
        ----------
        bm : :blender:`bpy.types.BMesh`
            The Blender mesh data structure.
        name : :obj:`str`, optional
            The name of the data block.
        free : :obj:`bool`, optional
            Free the data structure once the data block is created.

        Returns
        -------
        :class:`compas_blender.conversions.BlenderMesh`
        """
        data = bpy.data.meshes.new(name or 'Mesh')
        bm.to_mesh(data)
        if free:
            bm.free()
        mesh = cls()
        mesh.geometry = data
        return mesh

    @classmethod
    def from_monkey(cls, name=None):
        """Construct a Blender mesh wrappper from the Blender monkey.

        Parameters
        ----------
        name : :obj:`str`, optional
            The name of the data block.

        Returns
        -------
        :class:`compas_blender.conversions.BlenderMesh`
        """
        bm = bmesh.new()
        bmesh.ops.create_monkey(bm)
        data = bpy.data.meshes.new(name or 'Mesh')
        bm.to_mesh(data)
        bm.free()
        mesh = cls()
        mesh.geometry = data
        return mesh

    def to_compas(self, cls=None):
        """Convert the Blender mesh to a COMPAS mesh.

        Parameters
        ----------
        cls : :class:`compas.datastructures.Mesh`, optional
            The type of COMPAS mesh.

        Returns
        -------
        :class:`compas.datastructure.Mesh`
        """
        cls = cls or Mesh
        return cls.from_vertices_and_faces(self.vertices, self.faces)

    # def get_vertex_coordinates(self, vertex):
    #     return add_vectors(self.location, self.geometry.vertices[vertex].co)

    # def get_vertices_coordinates(self):
    #     xyzs = [vertex.co for vertex in self.geometry.vertices]
    #     return {vertex: add_vectors(self.location, xyz) for vertex, xyz in enumerate(xyzs)}

    # def set_vertices_coordinates(self, xyzs):
    #     for vertex, xyz in xyzs.items():
    #         self.geometry.vertices[vertex].co = subtract_vectors(xyz, self.location)

    # def get_vertices_colors(self, vertices=None):
    #     colors = {}
    #     col = self.geometry.vertex_colors.active
    #     if col:
    #         if not vertices:
    #             vertices = range(len(self.geometry.vertices))
    #         for face in self.geometry.polygons:
    #             for i in face.loop_indices:
    #                 j = self.geometry.loops[i].vertex_index
    #                 if (j in vertices) and (not colors.get(j, None)):
    #                     colors[j] = list(col.data[i].color)[:3]
    #         return colors

    # def set_vertices_colors(self, colors):
    #     if self.geometry.vertex_colors:
    #         col = self.geometry.vertex_colors.active
    #     else:
    #         col = self.geometry.vertex_colors.new()
    #     for face in self.geometry.polygons:
    #         for i in face.loop_indices:
    #             j = self.geometry.loops[i].vertex_index
    #             if j in colors:
    #                 col.data[i].color = list(colors[j]) + [1]

    # def unset_vertices_colors(self):
    #     vertex_colors = self.geometry.vertex_colors
    #     while vertex_colors:
    #         vertex_colors.remove(vertex_colors[0])

    # def get_edge_vertex_indices(self, edge):
    #     return list(self.geometry.edges[edge].vertices)

    # def get_edges_vertex_indices(self, edges=None):
    #     if not edges:
    #         edges = range(len(self.geometry.edges))
    #     return {edge: self.get_edge_vertex_indices(edge=edge) for edge in edges}

    # def edge_length(self, edge):
    #     u, v = self.geometry.edges[edge].vertices
    #     sp, ep = [list(self.geometry.vertices[i].co) for i in [u, v]]
    #     return distance_point_point(sp, ep)

    # def edges_lengths(self, edges=None):
    #     if not edges:
    #         edges = range(len(self.geometry.edges))
    #     return {edge: self.edge_length(edge=edge) for edge in edges}

    # def get_face_vertex_indices(self, face):
    #     return list(self.geometry.polygons[face].vertices)

    # def get_faces_vertex_indices(self, faces=None):
    #     if not faces:
    #         faces = range(len(self.geometry.polygons))
    #     return {face: self.get_face_vertex_indices(face=face) for face in faces}

    # def face_normal(self, face):
    #     return list(self.geometry.polygons[face].normal)

    # def faces_normals(self, faces=None):
    #     if not faces:
    #         faces = range(len(self.geometry.polygons))
    #     return {face: self.face_normal(face=face) for face in faces}

    # def face_area(self, face):
    #     return self.geometry.polygons[face].area

    # def faces_areas(self, faces=None):
    #     if not faces:
    #         faces = range(len(self.geometry.polygons))
    #     return {face: self.face_area(face=face) for face in faces}

    # def bevel(self, width=0.2, segments=1, only_vertices=False):
    #     self.object.modifiers.new('bevel', type='BEVEL')
    #     self.object.modifiers['bevel'].width = width
    #     self.object.modifiers['bevel'].segments = segments
    #     self.object.modifiers['bevel'].use_only_vertices = only_vertices
    #     self.refresh()

    # def subdivide(self, levels=1, type='SIMPLE'):
    #     self.object.modifiers.new('subdivision', type='SUBSURF')
    #     self.object.modifiers['subdivision'].levels = levels
    #     self.object.modifiers['subdivision'].subdivision_type = type  # or 'CATMULL_CLARK'
    #     self.refresh()

    # def triangulate(self):
    #     self.object.modifiers.new('triangulate', type='TRIANGULATE')
    #     self.refresh()

    # def get_vertices_and_faces(self):
    #     vertices = self.get_vertices_coordinates()
    #     faces = self.get_faces_vertex_indices()
    #     return vertices, faces


# src/compas_blender/geometry/__init__.py

"""
********************************************************************************
geometry
********************************************************************************

.. currentmodule:: compas_blender.geometry

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:


Plugins
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    booleans.boolean_difference_mesh_mesh
    booleans.boolean_intersection_mesh_mesh
    booleans.boolean_union_mesh_mesh

"""


# src/compas_blender/ui/__init__.py

"""
********************************************************************************
ui
********************************************************************************

.. currentmodule:: compas_blender.ui

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

"""
from .mouse import Mouse


__all__ = ['Mouse']


# src/compas_blender/ui/mouse.py

import bpy


__all__ = [
    'Mouse',
]


class Mouse(object):

    def __init__(self):
        pass

    def OnMouseMove(self, e):
        raise NotImplementedError

    def OnMouseDown(self, e):
        raise NotImplementedError

    def OnMouseUp(self, e):
        raise NotImplementedError

    def xyz(self):
        return list(bpy.context.scene.cursor_location.copy())


# src/compas_blender/utilities/__init__.py

"""
********************************************************************************
utilities
********************************************************************************

.. currentmodule:: compas_blender.utilities

This package contains low-level functions for working and interacting with Blender.


collections
===========

.. autosummary::
    :toctree: generated/
    :nosignatures:

    create_collection
    create_collections
    create_collections_from_path
    clear_collection
    clear_collections


data
====

.. autosummary::
    :toctree: generated/
    :nosignatures:

    delete_unused_data


drawing
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    draw_points
    draw_pointcloud
    draw_lines
    draw_cylinders
    draw_spheres
    draw_cubes
    draw_mesh
    draw_faces
    draw_texts


objects
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    delete_object
    delete_objects
    delete_object_by_name
    delete_objects_by_names
    get_object_by_name
    get_objects_by_names

"""
from .data import delete_unused_data
from .objects import (
    delete_object,
    delete_objects,
    delete_all_objects,
    delete_object_by_name,
    delete_objects_by_names,
    get_object_by_name,
    get_objects_by_names
)
from .collections import (
    create_collection,
    create_collections,
    create_collections_from_path,
    clear_collection,
    clear_collections
)
from .drawing import (
    draw_circles,
    draw_cylinders,
    draw_cubes,
    draw_faces,
    draw_lines,
    draw_mesh,
    draw_pipes,
    draw_planes,
    draw_pointcloud,
    draw_points,
    draw_polylines,
    draw_spheres,
    draw_texts,
    RGBColor,
)
from .misc import unload_modules


__all__ = [
    'delete_unused_data',
    'delete_object',
    'delete_objects',
    'delete_all_objects',
    'delete_object_by_name',
    'delete_objects_by_names',
    'get_object_by_name',
    'get_objects_by_names',
    'create_collection',
    'create_collections',
    'create_collections_from_path',
    'clear_collection',
    'clear_collections',
    'draw_circles',
    'draw_cylinders',
    'draw_cubes',
    'draw_faces',
    'draw_lines',
    'draw_mesh',
    'draw_pipes',
    'draw_planes',
    'draw_pointcloud',
    'draw_points',
    'draw_polylines',
    'draw_spheres',
    'draw_texts',
    'RGBColor',
    'unload_modules'
]


# src/compas_blender/utilities/collections.py

import bpy
from typing import List, Text

from compas_blender.utilities import delete_objects


__all__ = [
    "create_collection",
    "create_collections",
    "create_collections_from_path",
    "clear_collection",
    "clear_collections"
]


def collection_path(collection, names=[]):
    for parent in bpy.data.collections:
        if collection.name in parent.children:
            names.append(parent.name)
            collection_path(parent, names)
    return names


def create_collection(name: Text, parent: bpy.types.Collection = None) -> bpy.types.Collection:
    """Create a collection with the given name.

    Parameters
    ----------
    name : str
        The name of the collection.
    parent : bpy.types.Collection, optional
        A parent collection.

    Returns
    -------
    :blender:`bpy.types.Collection`

    """
    if not name:
        return

    if not parent:

        if name in bpy.data.collections:
            count = 1
            newname = f'{name}.{count:04}'
            while newname in bpy.data.collections:
                count += 1
                newname = f'{name}.{count:04}'
            name = newname
        collection = bpy.data.collections.new(name)
        bpy.context.scene.collection.children.link(collection)
    else:
        path = collection_path(parent)[::-1] + [parent.name]
        name = "::".join(path) + "::" + name
        if name not in parent.children:
            collection = bpy.data.collections.new(name)
            parent.children.link(collection)
        else:
            collection = bpy.data.collections.get(name)
    return collection


def create_collections(names: List[Text]) -> List[bpy.types.Collection]:
    """Create multiple collections at once.

    Parameters
    ----------
    names : list[str]
        Collection names.

    Returns
    -------
    list[:blender:`bpy.types.Collection`]

    """
    collections = [create_collection(name) for name in names]
    return collections


def create_collections_from_path(path: Text, separator: Text = '::') -> List[bpy.types.Collection]:
    """Create nested collections from a collection path string.

    Parameters
    ----------
    path : str
        The collection path with collection names separated by the specified separator.
    separator : str, optional
        The separator between components of the path.

    Returns
    -------
    list[:blender:`bpy.types.Collection`]

    """
    names = path.split(separator)
    collections = []
    parent = None
    for name in names:
        collection = create_collection(name, parent=parent)
        parent = collection
        collections.append(collection)
    return collections


def clear_collection(name: Text):
    """Clear the objects from a collection.

    Parameters
    ----------
    name : str
        The name of the collection.

    Returns
    -------
    None

    """
    objects = list(bpy.data.collections[name].objects)
    if objects:
        delete_objects(objects)


def clear_collections(collections: List[bpy.types.Collection]):
    """Clear the objects from multiple collections.

    Parameters
    ----------
    collections : list[:blender:`bpy.types.Collection`]
        A list of collections.

    Returns
    -------
    None

    """
    for name in collections:
        clear_collection(name)


# src/compas_blender/utilities/data.py

import bpy


__all__ = [
    "delete_unused_data",
]


def delete_unused_data():
    """Delete all collections, mesh and curve objects, meshes, curves, materials.

    Returns
    -------
    None

    """
    for block in bpy.data.meshes:
        if block.users == 0:
            bpy.data.meshes.remove(block)

    for block in bpy.data.curves:
        if block.users == 0:
            bpy.data.curves.remove(block)

    for block in bpy.data.materials:
        if block.users == 0:
            bpy.data.materials.remove(block)

    for block in bpy.data.textures:
        if block.users == 0:
            bpy.data.textures.remove(block)

    for block in bpy.data.images:
        if block.users == 0:
            bpy.data.images.remove(block)

    # for collection in bpy.context.scene.collection.children:
    #     bpy.context.scene.collection.children.unlink(collection)

    # for block in bpy.data.collections:
    #     objects = [o for o in block.objects if o.users]
    #     while objects:
    #         bpy.data.objects.remove(objects.pop())
    #     for collection in block.children:
    #         block.children.unlink(collection)
    #     if block.users == 0:
    #         bpy.data.collections.remove(block)


# src/compas_blender/utilities/drawing.py

import bpy

from typing import Dict
from typing import List
from typing import Union
from typing import Tuple
from typing import Text

from compas_blender.utilities import create_collection

from compas.geometry import centroid_points
from compas.geometry import distance_point_point
from compas.geometry import subtract_vectors


__all__ = [
    'draw_circles',
    'draw_cylinders',
    'draw_cubes',
    'draw_faces',
    'draw_lines',
    'draw_mesh',
    'draw_pipes',
    'draw_planes',
    'draw_pointcloud',
    'draw_points',
    'draw_polylines',
    'draw_spheres',
    'draw_texts',
    'RGBColor',
]


RGBColor = Union[Tuple[int, int, int], Tuple[float, float, float]]


def _link_object(obj, collection=None, layer=None):
    if not collection:
        collection = bpy.context.collection
    if not isinstance(collection, bpy.types.Collection):
        collection = create_collection(collection)
    # if not layer:
    #     layer = bpy.context.view_layer
    # layer_collection = layer.active_layer_collection.collection
    for c in obj.users_collection:
        c.objects.unlink(obj)
    collection.objects.link(obj)
    # layer_collection.objects.link(obj)


def _link_objects(objects, collection=None, layer=None):
    if not collection:
        collection = bpy.context.collection
    if not isinstance(collection, bpy.types.Collection):
        collection = create_collection(collection)
    # if not layer:
    #     layer = bpy.context.view_layer
    # layer_collection = layer.active_layer_collection.collection

    for o in objects:
        for c in o.users_collection:
            c.objects.unlink(o)
        collection.objects.link(o)
        # layer_collection.objects.link(o)


def _create_material(rgb, alpha=1.0):
    rgba = list(rgb) + [alpha]
    name = '-'.join(['{0:.2f}'.format(i) for i in rgba])
    material = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    material.diffuse_color = rgba
    return material


def _set_object_color(obj, rgb, alpha=1.0):
    rgba = list(rgb) + [alpha]
    material = _create_material(rgb, alpha)
    obj.color = rgba
    if obj.data.materials:
        obj.data.materials[0] = material
    else:
        obj.data.materials.append(material)
    obj.active_material = material


# ==============================================================================
# Annotations
# ==============================================================================


def draw_texts(texts: List[Dict],
               collection: Union[Text, bpy.types.Collection] = None,
               color: Tuple[float, float, float] = (1.0, 1.0, 1.0)) -> List[bpy.types.Object]:
    """Draw text objects.

    Parameters
    ----------
    texts : list[dict]
        A list of dicts describing the text objects.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.
    color : tuple[int, int, int] or tuple[float, float, float], optional
        The text color.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    bpy.ops.object.text_add()
    empty = bpy.context.object
    _link_object(empty, collection)
    objects = [0] * len(texts)
    for index, data in enumerate(texts):
        obj = empty.copy()
        obj.data = empty.data.copy()
        _set_object_color(obj, data.get('color', color))
        obj.location = data['pos']
        obj.data.body = data['text']
        obj.scale *= data.get('size', 1)
        obj.name = data.get('name', 'text')
        objects[index] = obj
    _link_objects(objects, collection)
    empty.hide_set(True)
    return objects


# ==============================================================================
# Primitives
# ==============================================================================


# replace this by a custom point shader
# https://docs.blender.org/api/current/gpu.html#custom-shader-for-dotted-3d-line
# https://docs.blender.org/api/current/gpu.html#triangle-with-custom-shader
def draw_points(points: List[Dict],
                collection: Union[Text, bpy.types.Collection] = None) -> List[bpy.types.Object]:
    """Draw point objects.

    Parameters
    ----------
    points : list[dict]
        A list of dicts describing the points.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    P = len(points)
    N = len(str(P))
    add_point = bpy.ops.mesh.primitive_uv_sphere_add
    objects = [0] * P
    for index, point in enumerate(points):
        xyz = point['pos']
        radius = point.get('radius', 1.0)
        name = point.get('name', f'P.{index:0{N}d}')
        color = list(point.get('color', [1.0, 1.0, 1.0]))
        add_point(location=xyz, radius=radius, segments=10, ring_count=10)
        obj = bpy.context.object
        obj.name = name
        # values = [True] * len(obj.data.polygons)
        # obj.data.polygons.foreach_set("use_smooth", values)
        _set_object_color(obj, color)
        objects[index] = obj
    _link_objects(objects, collection)
    return objects


# replace this by a custom pointcloud shader
# https://docs.blender.org/api/current/gpu.html#custom-shader-for-dotted-3d-line
# https://docs.blender.org/api/current/gpu.html#triangle-with-custom-shader
def draw_pointcloud(points: List[Dict],
                    collection: Union[Text, bpy.types.Collection] = None) -> bpy.types.Object:
    """Draw point objects as a single cloud.

    Parameters
    ----------
    points : list[dict]
        A list of dicts describing the points of the pointcloud.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    P = len(points)
    N = len(str(P))
    bpy.ops.mesh.primitive_uv_sphere_add(location=[0, 0, 0], radius=1.0, segments=10, ring_count=10)
    empty = bpy.context.object
    _link_object(empty, collection)
    _set_object_color(empty, [1.0, 1.0, 1.0])
    objects = [0] * P
    for index, data in enumerate(points):
        obj = empty.copy()
        obj.location = data['pos']
        obj.scale *= data.get('radius', 1.0)
        obj.name = data.get('name', f'P.{index:0{N}d}')
        # obj.data.polygons.foreach_set("use_smooth", [True] * len(obj.data.polygons))
        objects[index] = obj
    _link_objects(objects, collection)
    empty.hide_set(True)
    return objects


# replace this by a custom line shader
# https://docs.blender.org/api/current/gpu.html#custom-shader-for-dotted-3d-line
# https://docs.blender.org/api/current/gpu.html#triangle-with-custom-shader
def draw_lines(lines: List[Dict],
               collection: Union[Text, bpy.types.Collection] = None,
               centroid: bool = True) -> List[bpy.types.Object]:
    """Draw line objects.

    Parameters
    ----------
    lines : list[dict]
        A list of dicts describing the lines.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.
    centroid : bool, optional
        If True, use the centroids of the lines as the relative base for their coordinates,
        instead of the origin of the world coordinates system.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    L = len(lines)
    N = len(str(L))
    objects = [0] * L
    for index, data in enumerate(lines):
        sp = data['start']
        ep = data['end']
        origin = centroid_points([sp, ep]) if centroid else [0, 0, 0]
        name = data.get('name', f'L.{index:0{N}d}')
        curve = bpy.data.curves.new(name, type='CURVE')
        curve.dimensions = '3D'
        spline = curve.splines.new('POLY')
        spline.points.add(1)
        spline.points[0].co = subtract_vectors(sp, origin) + [1.0]
        spline.points[1].co = subtract_vectors(ep, origin) + [1.0]
        spline.order_u = 1
        obj = bpy.data.objects.new(name, curve)
        obj.location = origin
        obj.data.fill_mode = 'FULL'
        obj.data.bevel_depth = data.get('width', 0.05)
        obj.data.bevel_resolution = 0
        obj.data.resolution_u = 20
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    return objects


# replace this by a custom polyline shader
# https://docs.blender.org/api/current/gpu.html#custom-shader-for-dotted-3d-line
# https://docs.blender.org/api/current/gpu.html#triangle-with-custom-shader
def draw_polylines(polylines: List[Dict],
                   collection: Union[Text, bpy.types.Collection] = None,
                   centroid: bool = True) -> List[bpy.types.Object]:
    """Draw polyline objects.

    Parameters
    ----------
    polylines : list[dict]
        A list of dicts describing the polylines.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.
    centroid : bool, optional
        If True, use the centroids of the polylines as the relative base for their coordinates,
        instead of the origin of the world coordinates system.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    P = len(polylines)
    N = len(str(P))
    objects = [0] * P
    for index, data in enumerate(polylines):
        points = data['points']
        origin = centroid_points(points) if centroid else [0, 0, 0]
        name = data.get('name', f'POLY.{index:0{N}d}')
        curve = bpy.data.curves.new(name, type='CURVE')
        curve.dimensions = '3D'
        spline = curve.splines.new('POLY')
        spline.points.add(len(points) - 1)
        for i, point in enumerate(points):
            spline.points[i].co = subtract_vectors(point, origin) + [1.0]
        spline.order_u = 1
        obj = bpy.data.objects.new(name, curve)
        obj.location = origin
        obj.data.fill_mode = 'FULL'
        obj.data.bevel_depth = data.get('width', 0.05)
        obj.data.bevel_resolution = 0
        obj.data.resolution_u = 20
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    return objects


def draw_polygons(polygons: List[Dict],
                  collection: Union[Text, bpy.types.Collection] = None,
                  centroid: bool = True) -> List[bpy.types.Object]:
    """Draw polyline objects.

    Parameters
    ----------
    polygons : list[dict]
        A list of dicts describing the polygons.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.
    centroid : bool, optional
        If True, use the centroids of the polygons as the relative base for their coordinates,
        instead of the origin of the world coordinates system.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    raise NotImplementedError


def draw_curves(curves: List[Dict],
                collection: Union[Text, bpy.types.Collection] = None,
                centroid: bool = True) -> List[bpy.types.Object]:
    """Draw curve objects.

    Parameters
    ----------
    curves : list[dict]
        A list of dicts describing the curves.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.
    centroid : bool, optional
        If True, use the centroids of the curves as the relative base for their coordinates,
        instead of the origin of the world coordinates system.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    raise NotImplementedError


def draw_faces(faces: List[Dict],
               collection: Union[Text, bpy.types.Collection] = None) -> List[bpy.types.Object]:
    """Draw polygonal faces.

    Parameters
    ----------
    faces : list[dict]
        A list of dicts describing the faces.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    F = len(faces)
    N = len(str(F))
    objects = [0] * F
    for index, face in enumerate(faces):
        points = face['points']
        indices = [list(range(len(points)))]
        name = face.get('name', f'FACE.{index:0{N}d}')
        color = face.get('color', [1.0, 1.0, 1.0])
        obj = draw_mesh(name=name, vertices=points, faces=indices, color=color, collection=collection)
        objects[index] = obj
    return objects


def draw_circles(circles: List[Dict],
                 collection: Union[Text, bpy.types.Collection] = None) -> List[bpy.types.Object]:
    """Draw circle objects as mesh primitives.

    Parameters
    ----------
    circles : list[dict]
        A list of dicts describing the circles.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    from math import acos
    from math import atan2
    bpy.ops.mesh.primitive_circle_add()
    empty = bpy.context.object
    _link_object(empty, collection)
    objects = [0] * len(circles)
    for index, data in enumerate(circles):
        obj = empty.copy()
        obj.name = data.get('name', 'circle')
        radius = data.get('radius', 1.0)
        point, normal = data.get('plane', ((0.0, 0.0, 0.0), (0.0, 0.0, 1.0)))
        obj.rotation_euler[1] = acos(normal[2])
        obj.rotation_euler[2] = atan2(normal[1], normal[0])
        obj.location = point
        obj.scale = ((radius, radius, 1.0))
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    empty.hide_set(True)
    return objects


def draw_planes(planes: List[Dict],
                collection: Union[Text, bpy.types.Collection] = None) -> List[bpy.types.Object]:
    """Draw plane objects as mesh primitives.

    Parameters
    ----------
    planes : list[dict]
        A list of dicts describing the planes.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    from math import acos
    from math import atan2
    bpy.ops.mesh.primitive_plane_add()
    empty = bpy.context.object
    _link_object(empty, collection)
    objects = [0] * len(planes)
    for index, data in enumerate(planes):
        obj = empty.copy()
        obj.name = data.get('name', 'plane')
        point, normal = data.get('plane', ((0.0, 0.0, 0.0), (0.0, 0.0, 1.0)))
        obj.rotation_euler[1] = acos(normal[2])
        obj.rotation_euler[2] = atan2(normal[1], normal[0])
        obj.location = point
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    empty.hide_set(True)
    return objects


# ==============================================================================
# Shapes
# ==============================================================================


def draw_cylinders(cylinders: List[Dict],
                   collection: Union[Text, bpy.types.Collection] = None,
                   uv: int = 10) -> List[bpy.types.Object]:
    """Draw cylinder objects as mesh primitives.

    Parameters
    ----------
    cylinders : list[dict]
        A list of dicts describing the cylinders.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    from math import acos
    from math import atan2
    bpy.ops.mesh.primitive_cylinder_add(location=[0, 0, 0], radius=1, depth=1, vertices=uv)
    empty = bpy.context.object
    _link_object(empty, collection)
    objects = [0] * len(cylinders)
    for index, data in enumerate(cylinders):
        sp = data['start']
        ep = data['end']
        mp = centroid_points([sp, ep])
        radius = data.get('radius', 1.0)
        length = distance_point_point(sp, ep)
        obj = empty.copy()
        obj.name = data.get('name', 'cylinder')
        obj.rotation_euler[1] = acos((ep[2] - sp[2]) / length)
        obj.rotation_euler[2] = atan2(ep[1] - sp[1], ep[0] - sp[0])
        obj.location = mp
        obj.scale = ((radius, radius, length))
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    empty.hide_set(True)
    return objects


# these objects are all linked.
# therefore they cannot have different colors
# also, if the linked mesh data block is changed, it will affect all objects
def draw_spheres(spheres: List[Dict],
                 collection: Union[Text, bpy.types.Collection] = None,
                 uv: int = 10) -> List[bpy.types.Object]:
    """Draw sphere objects as mesh primitives.

    Parameters
    ----------
    spheres : list[dict]
        A list of dicts describing the spheres.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    bpy.ops.mesh.primitive_uv_sphere_add(location=[0, 0, 0], radius=1.0, segments=uv, ring_count=uv)
    empty = bpy.context.object
    _link_object(empty, collection)
    objects = [0] * len(spheres)
    for index, data in enumerate(spheres):
        obj = empty.copy()
        obj.location = data['pos']
        obj.scale *= data.get('radius', 1.0)
        obj.name = data.get('name', 'sphere')
        # values = [True] * len(obj.data.polygons)
        # obj.data.polygons.foreach_set("use_smooth", values)
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    empty.hide_set(True)
    return objects


def draw_cubes(cubes: List[Dict],
               collection: Union[Text, bpy.types.Collection] = None) -> List[bpy.types.Object]:
    """Draw cube objects as mesh primitives.

    Parameters
    ----------
    cubes : list[dict]
        A list of dicts describing the cubes.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    bpy.ops.mesh.primitive_cube_add(size=1, location=[0, 0, 0])
    empty = bpy.context.object
    _link_object(empty, collection)
    objects = [0] * len(cubes)
    for index, data in enumerate(cubes):
        obj = empty.copy()
        obj.location = data['pos']
        obj.scale *= data.get('size', 1)
        obj.name = data.get('name', 'cube')
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    empty.hide_set(True)
    return objects


# replace this by a custom polyline shader
# https://docs.blender.org/api/current/gpu.html#custom-shader-for-dotted-3d-line
# https://docs.blender.org/api/current/gpu.html#triangle-with-custom-shader
def draw_pipes(pipes: List[Dict],
               collection: Union[Text, bpy.types.Collection] = None,
               centroid: bool = True,
               smooth: bool = True) -> List[bpy.types.Object]:
    """Draw polyline objects.

    Parameters
    ----------
    pipes : list[dict]
        A list of dicts describing the pipes.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.
    centroid : bool, optional
        If True, use the centroids of the pipes as the relative base for their coordinates,
        instead of the origin of the world coordinates system.
    smooth : bool, optional
        If True, use a NURBS curve instead of a polyline.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    P = len(pipes)
    N = len(str(P))
    objects = [0] * P
    for index, data in enumerate(pipes):
        points = data['points']
        origin = centroid_points(points) if centroid else [0, 0, 0]
        name = data.get('name', f'POLY.{index:0{N}d}')
        curve = bpy.data.curves.new(name, type='CURVE')
        curve.dimensions = '3D'
        curve.fill_mode = 'FULL'
        curve.bevel_depth = data.get('width', 0.05)
        curve.bevel_resolution = 0
        curve.resolution_u = 20
        curve.use_fill_caps = True
        if smooth:
            spline = curve.splines.new('NURBS')
        else:
            spline = curve.splines.new('POLY')
        spline.points.add(len(points) - 1)
        for i, point in enumerate(points):
            spline.points[i].co = subtract_vectors(point, origin) + [1.0]
        spline.order_u = 1
        obj = bpy.data.objects.new(name, curve)
        obj.location = origin
        rgb = data.get('color', [1.0, 1.0, 1.0])
        _set_object_color(obj, rgb)
        objects[index] = obj
    _link_objects(objects, collection)
    return objects


# ==============================================================================
# Data Structures
# ==============================================================================


def draw_mesh(vertices: List[List[float]],
              faces: List[List[int]],
              name: str = 'mesh',
              color: Tuple[float, float, float] = (1.0, 1.0, 1.0),
              centroid: bool = True,
              collection: Union[Text, bpy.types.Collection] = None, **kwargs) -> bpy.types.Object:
    """Draw a mesh object.

    Parameters
    ----------
    vertices : list[[float, float, float] or :class:`compas.geometry.Point`]
        The vertices of the mesh.
    faces : list[list[int]]
        The faces of the mesh.
    color : tuple[float, float, float], optional
        The color of the mesh.
    centroid : bool, optional
        If True, use the centroid of the mesh as the relative base for the vertex coordinates,
        instead of the origin of the world coordinates system.
    collection : str or :blender:`bpy.types.Collection`, optional
        The Blender scene collection that should contain the objects created by this function.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    mp = centroid_points(vertices) if centroid else [0, 0, 0]
    vertices = [subtract_vectors(vertex, mp) for vertex in vertices]
    mesh = bpy.data.meshes.new(name)
    mesh.from_pydata(vertices, [], faces)
    mesh.update(calc_edges=True)
    obj = bpy.data.objects.new(name, mesh)
    obj.show_wire = True
    obj.location = mp
    _set_object_color(obj, color)
    _link_objects([obj], collection=collection)
    return obj


# src/compas_blender/utilities/misc.py

import sys


__all__ = [
    'unload_modules',
]


def unload_modules(top_level_module_name):
    """Unloads all modules named starting with the specified string.

    This function eases the development workflow when editing a library that is
    used from Blender.

    Parameters
    ----------
    top_level_module_name : str
        Name of the top-level module to unload.

    Returns
    -------
    list[str]
        List of unloaded module names.

    """
    modules = list(filter(lambda m: m.startswith(top_level_module_name), sys.modules))

    for module in modules:
        sys.modules.pop(module)

    return modules


# src/compas_blender/utilities/objects.py

import bpy
from typing import List, Iterable, Text

from compas_blender.utilities.data import delete_unused_data


__all__ = [
    "delete_object",
    "delete_objects",
    "delete_all_objects",
    "delete_object_by_name",
    "delete_objects_by_names",
    "get_object_by_name",
    "get_objects_by_names",
]


# ==============================================================================
# Delete
# ==============================================================================


def delete_all_objects(purge_data: bool = True):
    """Delete all mesh and curve scene objects, and the attached mesh and curve data blocks.

    Parameters
    ----------
    purge_data : bool, optional
        If True, also delete the underlying data blocks.

    Returns
    -------
    None

    """
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=True, confirm=False)
    if purge_data:
        delete_unused_data()


def delete_object(obj: bpy.types.Object, purge_data: bool = True):
    """Delete a scene object.

    Parameters
    ----------
    obj : :blender:`bpy.types.Object`
        The Blender scene object.
    purge_data : bool, optional
        If True, also delete the underlying data blocks.

    Returns
    -------
    None

    """
    bpy.data.objects.remove(obj)
    if purge_data:
        delete_unused_data()


def delete_objects(objects: Iterable[bpy.types.Object], purge_data: bool = True):
    """Delete multiple scene objects.

    Parameters
    ----------
    objects : list[:blender:`bpy.types.Object`]
        A list of scene objects.
    purge_data : bool, optional
        If True, also delete the underlying data blocks.

    Returns
    -------
    None

    """
    for obj in objects:
        delete_object(obj, purge_data=False)
    if purge_data:
        delete_unused_data()


def delete_object_by_name(name: str):
    """Delete the scene object corresponding to the given name.

    Parameters
    ----------
    name : str
        The name of the Blender scene object.

    Returns
    -------
    None

    """
    obj = get_object_by_name(name)
    delete_object(obj)


def delete_objects_by_names(names: Iterable[Text]):
    """Delete the scene objects corresponding to the list of names.

    Parameters
    ----------
    names : list[str]
        The names of the Blender scene object.

    Returns
    -------
    None

    """
    objects = get_objects_by_names(names)
    delete_objects(objects)


# ==============================================================================
# Get
# ==============================================================================


def get_object_by_name(name: str) -> bpy.types.Object:
    """Get the object with the given name.

    Parameters
    ----------
    name : str
        The name of the Blender scene object.

    Returns
    -------
    :blender:`bpy.types.Object`

    """
    return bpy.data.objects[name]


def get_objects_by_names(names: Iterable[str]) -> List[bpy.types.Object]:
    """Get the objects corresponding to the given names.

    Parameters
    ----------
    names : list[str]
        The names of the Blender scene object.

    Returns
    -------
    list[:blender:`bpy.types.Object`]

    """
    return [bpy.data.objects[name] for name in names]


# def get_objects(names=None, color=None, layer=None, type=None):
#     if names:
#         objects = get_objects_by_names(names)
#     elif color:
#         raise NotImplementedError
#     elif layer:
#         objects = list(bpy.data.collections[layer].objects)
#     elif type:
#         objects = [obj for obj in bpy.data.objects if obj.type == type.upper()]
#     else:
#         objects = list(bpy.data.objects)
#     return objects


# def get_object_name(obj):
#     return obj.name


# def get_objects_names(objects):
#     return [get_object_name(obj) for obj in objects]


# def get_objects_layers(objects):
#     return [obj.users_collection for obj in objects]


# def get_objects_types(objects):
#     return [obj.type for obj in objects]


# def get_objects_coordinates(objects):
#     return [list(obj.location) for obj in objects]


# def get_object_property(obj, property_name):
#     return obj.get(property_name)


# def get_objects_property(objects, property_name):
#     return [get_object_property(obj, property_name) for obj in objects]


# def select_object(message="Select an object."):
#     raise NotImplementedError


# def select_objects(message="Select objects."):
#     raise NotImplementedError


# ==============================================================================
# Points
# ==============================================================================


# def get_points(layer=None):
#     return [obj for obj in get_objects(layer=layer) if obj.type == "EMPTY"]


# def get_points_coordinates(objects=None):
#     if objects is None:
#         objects = get_points()
#     return [list(obj.location) for obj in objects if obj.type == "EMPTY"]


# def select_point(message="Select a point."):
#     raise NotImplementedError


# def select_points(message="Select points."):
#     raise NotImplementedError


# ==============================================================================
# Curves
# ==============================================================================


# def select_curve(message="Select curve."):
#     raise NotImplementedError


# def select_curves(message="Select curves."):
#     raise NotImplementedError


# def get_curves(layer=None):
#     return [obj for obj in get_objects(layer=layer) if obj.type == "CURVE"]


# def get_curves_coordinates(objects):
#     raise NotImplementedError


# ==============================================================================
# Meshes
# ==============================================================================


# def select_mesh(message="Select a mesh."):
#     raise NotImplementedError


# def select_meshes(message="Select meshes."):
#     raise NotImplementedError


# def get_meshes(layer=None):
#     return [obj for obj in get_objects(layer=layer) if obj.type == "MESH"]


# def mesh_from_bmesh(bmesh):
#     vertices = [list(vertex.co) for vertex in bmesh.data.vertices]
#     faces = [list(face.vertices) for face in bmesh.data.polygons]
#     return Mesh.from_vertices_and_faces(vertices=vertices, faces=faces)


# ==============================================================================
# Surfaces
# ==============================================================================


# ==============================================================================
# Set
# ==============================================================================

# def set_select(objects=None):
#     if objects:
#         for obj in objects:
#             obj.select_set(state=True)
#     else:
#         bpy.ops.object.select_all(action="SELECT")


# def set_deselect(objects=None):
#     if objects:
#         for obj in objects:
#             obj.select_set(state=False)
#     else:
#         bpy.ops.object.select_all(action="DESELECT")


# def set_objects_layer(objects, layer):
#     collection_new = bpy.data.collections.get(layer) or bpy.data.collections.new(layer)
#     for obj in objects:
#         for collection_old in obj.users_collection:
#             collection_old.objects.unlink(obj)
#         collection_new.objects.link(obj)


# def set_objects_coordinates(objects, coords):
#     for obj, xyz in zip(objects, coords):
#         obj.location = xyz


# def set_objects_rotations(objects, rotations):
#     for obj, r in zip(objects, rotations):
#         obj.rotation_euler = r


# def set_objects_scales(objects, scales):
#     for obj, s in zip(objects, scales):
#         obj.scale = s


# def set_objects_show_names(objects, show=True):
#     for obj in objects:
#         obj.show_name = show


# def set_objects_visible(objects, visible=True):
#     for obj in objects:
#         obj.hide_viewport = not visible


# def set_object_property(obj, property_name, property_value):
#     obj[property_name] = property_value


# def set_objects_property(objects, property_name, property_value):
#     for obj in objects:
#         obj[property_name] = property_value


# src/compas_ghpython/artists/__init__.py

"""
********************************************************************************
artists
********************************************************************************

.. currentmodule:: compas_ghpython.artists

.. rst-class:: lead

Artists for visualizing (painting) COMPAS geometry, robots, and data structures in Grasshopper using the GH Python component.
The artists in this package provide plugins for the pluggable methods of the base artists in :mod:`compas.artists`.
Therefore, they can be used directly, from here, or through the base artists :class:`compas.artists.Artist`.


Primitive Artists
=================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    BoxArtist
    CapsuleArtist
    CircleArtist
    ConeArtist
    CylinderArtist
    FrameArtist
    LineArtist
    PointArtist
    PolygonArtist
    PolyhedronArtist
    PolylineArtist
    SphereArtist
    TorusArtist
    VectorArtist


Datastructure Artists
=====================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    MeshArtist
    NetworkArtist
    VolMeshArtist


Robot Artist
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    RobotModelArtist


Base Classes
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    GHArtist

"""
from __future__ import absolute_import

from compas.plugins import plugin
from compas.artists import Artist
from compas.artists import ShapeArtist

from compas.geometry import Box
from compas.geometry import Capsule
from compas.geometry import Circle
from compas.geometry import Cone
from compas.geometry import Cylinder
from compas.geometry import Frame
from compas.geometry import Line
from compas.geometry import Point
from compas.geometry import Polygon
from compas.geometry import Polyhedron
from compas.geometry import Polyline
from compas.geometry import Sphere
from compas.geometry import Torus
from compas.geometry import Vector

from compas.datastructures import Mesh
from compas.datastructures import Network
from compas.datastructures import VolMesh

from compas.robots import RobotModel

from .artist import GHArtist
from .boxartist import BoxArtist
from .capsuleartist import CapsuleArtist
from .circleartist import CircleArtist
from .coneartist import ConeArtist
from .cylinderartist import CylinderArtist
from .frameartist import FrameArtist
from .lineartist import LineArtist
from .meshartist import MeshArtist
from .networkartist import NetworkArtist
from .pointartist import PointArtist
from .polygonartist import PolygonArtist
from .polyhedronartist import PolyhedronArtist
from .polylineartist import PolylineArtist
from .robotmodelartist import RobotModelArtist
from .sphereartist import SphereArtist
from .torusartist import TorusArtist
from .vectorartist import VectorArtist
from .volmeshartist import VolMeshArtist


ShapeArtist.default_color = (255, 255, 255)

MeshArtist.default_color = (0, 0, 0)
MeshArtist.default_vertexcolor = (255, 255, 255)
MeshArtist.default_edgecolor = (0, 0, 0)
MeshArtist.default_facecolor = (255, 255, 255)

NetworkArtist.default_nodecolor = (255, 255, 255)
NetworkArtist.default_edgecolor = (0, 0, 0)

VolMeshArtist.default_color = (0, 0, 0)
VolMeshArtist.default_vertexcolor = (255, 255, 255)
VolMeshArtist.default_edgecolor = (0, 0, 0)
VolMeshArtist.default_facecolor = (255, 255, 255)
VolMeshArtist.default_cellcolor = (255, 0, 0)


@plugin(category='factories', requires=['Rhino'])
def register_artists():
    Artist.register(Box, BoxArtist, context='Grasshopper')
    Artist.register(Capsule, CapsuleArtist, context='Grasshopper')
    Artist.register(Circle, CircleArtist, context='Grasshopper')
    Artist.register(Cone, ConeArtist, context='Grasshopper')
    Artist.register(Cylinder, CylinderArtist, context='Grasshopper')
    Artist.register(Frame, FrameArtist, context='Grasshopper')
    Artist.register(Line, LineArtist, context='Grasshopper')
    Artist.register(Mesh, MeshArtist, context='Grasshopper')
    Artist.register(Network, NetworkArtist, context='Grasshopper')
    Artist.register(Point, PointArtist, context='Grasshopper')
    Artist.register(Polygon, PolygonArtist, context='Grasshopper')
    Artist.register(Polyhedron, PolyhedronArtist, context='Grasshopper')
    Artist.register(Polyline, PolylineArtist, context='Grasshopper')
    Artist.register(RobotModel, RobotModelArtist, context='Grasshopper')
    Artist.register(Sphere, SphereArtist, context='Grasshopper')
    Artist.register(Torus, TorusArtist, context='Grasshopper')
    Artist.register(Vector, VectorArtist, context='Grasshopper')
    Artist.register(VolMesh, VolMeshArtist, context='Grasshopper')
    print('GH Artists registered.')


__all__ = [
    'GHArtist',
    'ShapeArtist',
    'BoxArtist',
    'CapsuleArtist',
    'CircleArtist',
    'ConeArtist',
    'CylinderArtist',
    'FrameArtist',
    'LineArtist',
    'MeshArtist',
    'NetworkArtist',
    'PointArtist',
    'PolygonArtist',
    'PolyhedronArtist',
    'PolylineArtist',
    'RobotModelArtist',
    'SphereArtist',
    'TorusArtist',
    'VectorArtist',
    'VolMeshArtist',
]


# src/compas_ghpython/artists/artist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.artists import Artist


class GHArtist(Artist):
    """Base class for all GH artists.
    """

    def __init__(self, **kwargs):
        super(GHArtist, self).__init__(**kwargs)


# src/compas_ghpython/artists/boxartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import ShapeArtist
from .artist import GHArtist


class BoxArtist(GHArtist, ShapeArtist):
    """Artist for drawing box shapes.

    Parameters
    ----------
    box : :class:`compas.geometry.Box`
        A COMPAS box.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.ShapeArtist` for more info.

    """

    def __init__(self, box, **kwargs):
        super(BoxArtist, self).__init__(shape=box, **kwargs)

    def draw(self, color=None):
        """Draw the box associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the box.

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        color = color or self.color
        vertices = [list(vertex) for vertex in self.shape.vertices]
        faces = self.shape.faces
        mesh = compas_ghpython.draw_mesh(vertices,
                                         faces,
                                         color=color)
        return mesh


# src/compas_ghpython/artists/capsuleartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import ShapeArtist
from .artist import GHArtist


class CapsuleArtist(GHArtist, ShapeArtist):
    """Artist for drawing capsule shapes.

    Parameters
    ----------
    capsule : :class:`compas.geometry.Capsule`
        A COMPAS capsule.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.ShapeArtist` for more info.

    """

    def __init__(self, capsule, **kwargs):
        super(CapsuleArtist, self).__init__(shape=capsule, **kwargs)

    def draw(self, color=None, u=None, v=None):
        """Draw the capsule associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the capsule.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`CapsuleArtist.u`.
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`CapsuleArtist.v`.

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        color = color or self.color
        u = u or self.u
        v = v or self.v
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        vertices = [list(vertex) for vertex in vertices]
        mesh = compas_ghpython.draw_mesh(vertices,
                                         faces,
                                         color=color)
        return mesh


# src/compas_ghpython/artists/circleartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import PrimitiveArtist
from .artist import GHArtist


class CircleArtist(GHArtist, PrimitiveArtist):
    """Artist for drawing circles.

    Parameters
    ----------
    circle : :class:`compas.geometry.Circle`
        A COMPAS circle.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    """

    def __init__(self, circle, **kwargs):
        super(CircleArtist, self).__init__(primitive=circle, **kwargs)

    def draw(self):
        """Draw the circle.

        Returns
        -------
        :rhino:`Rhino.Geometry.Circle`

        """
        circles = [self._get_args(self.primitive, self.color)]
        return compas_ghpython.draw_circles(circles)[0]

    @staticmethod
    def _get_args(primitive, color=None):
        point = list(primitive.plane.point)
        normal = list(primitive.plane.normal)
        radius = primitive.radius
        return {'plane': [point, normal], 'radius': radius, 'color': color, 'name': primitive.name}


# src/compas_ghpython/artists/coneartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import ShapeArtist
from .artist import GHArtist


class ConeArtist(GHArtist, ShapeArtist):
    """Artist for drawing cone shapes.

    Parameters
    ----------
    shape : :class:`compas.geometry.Cone`
        A COMPAS cone.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.ShapeArtist` for more info.

    """

    def __init__(self, cone, **kwargs):
        super(ConeArtist, self).__init__(shape=cone, **kwargs)

    def draw(self, color=None, u=None):
        """Draw the cone associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the cone.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`ConeArtist.u`

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        color = color or self.color
        u = u or self.u
        vertices, faces = self.shape.to_vertices_and_faces(u=u)
        vertices = [list(vertex) for vertex in vertices]
        mesh = compas_ghpython.draw_mesh(vertices,
                                         faces,
                                         color=color)
        return mesh


# src/compas_ghpython/artists/cylinderartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import ShapeArtist
from .artist import GHArtist


class CylinderArtist(GHArtist, ShapeArtist):
    """Artist for drawing cylinder shapes.

    Parameters
    ----------
    cylinder : :class:`compas.geometry.Cylinder`
        A COMPAS cylinder.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.ShapeArtist` for more info.

    """

    def __init__(self, cylinder, **kwargs):
        super(CylinderArtist, self).__init__(shape=cylinder, **kwargs)

    def draw(self, color=None, u=None):
        """Draw the cylinder associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the cylinder.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`CylinderArtist.u`

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        color = color or self.color
        u = u or self.u
        vertices, faces = self.shape.to_vertices_and_faces(u=u)
        vertices = [list(vertex) for vertex in vertices]
        mesh = compas_ghpython.draw_mesh(vertices,
                                         faces,
                                         color=color)
        return mesh


# src/compas_ghpython/artists/frameartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import PrimitiveArtist
from .artist import GHArtist


class FrameArtist(GHArtist, PrimitiveArtist):
    """Artist for drawing frames.

    Parameters
    ----------
    frame : :class:`compas.geometry.Frame`
        A COMPAS frame.
    scale : float, optional
        The scale of the vectors representing the axes of the frame.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    scale : float
        Scale factor that controls the length of the axes.
    color_origin : tuple[int, int, int]
        Default is ``(0, 0, 0)``.
    color_xaxis : tuple[int, int, int]
        Default is ``(255, 0, 0)``.
    color_yaxis : tuple[int, int, int]
        Default is ``(0, 255, 0)``.
    color_zaxis : tuple[int, int, int]
        Default is ``(0, 0, 255)``.

    """

    def __init__(self, frame, scale=1.0, **kwargs):
        super(FrameArtist, self).__init__(primitive=frame, **kwargs)
        self.scale = scale
        self.color_origin = (0, 0, 0)
        self.color_xaxis = (255, 0, 0)
        self.color_yaxis = (0, 255, 0)
        self.color_zaxis = (0, 0, 255)

    def draw(self):
        """Draw the frame.

        Returns
        -------
        :rhino:`Rhino.Geometry.Plane`

        """
        return compas_ghpython.draw_frame(self.primitive)

    def draw_origin(self):
        """Draw the frame's origin.

        Returns
        -------
        :rhino:`Rhino.Geometry.Point`

        """
        point, _ = self._get_args(self.primitive, self.scale, self.color_origin, self.color_xaxis, self.color_yaxis, self.color_zaxis)
        return compas_ghpython.draw_points([point])[0]

    def draw_axes(self):
        """Draw the frame's axes.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Line`]

        """
        _, lines = self._get_args(self.primitive, self.scale, self.color_origin, self.color_xaxis, self.color_yaxis, self.color_zaxis)
        return compas_ghpython.draw_lines(lines)

    @staticmethod
    def _get_args(primitive, scale=1.0, color_origin=(0, 0, 0), color_xaxis=(255, 0, 0), color_yaxis=(0, 255, 0), color_zaxis=(0, 0, 255)):
        origin = list(primitive.point)
        x = list(primitive.point + primitive.xaxis.scaled(scale))
        y = list(primitive.point + primitive.yaxis.scaled(scale))
        z = list(primitive.point + primitive.zaxis.scaled(scale))
        point = {'pos': origin, 'color': color_origin}
        lines = [
            {'start': origin, 'end': x, 'color': color_xaxis, 'arrow': 'end'},
            {'start': origin, 'end': y, 'color': color_yaxis, 'arrow': 'end'},
            {'start': origin, 'end': z, 'color': color_zaxis, 'arrow': 'end'}]
        return point, lines


# src/compas_ghpython/artists/lineartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import PrimitiveArtist
from .artist import GHArtist


class LineArtist(GHArtist, PrimitiveArtist):
    """Artist for drawing lines.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`
        A COMPAS line.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    """

    def __init__(self, line, **kwargs):
        super(LineArtist, self).__init__(primitive=line, **kwargs)

    def draw(self):
        """Draw the line.

        Returns
        -------
        :rhino:`Rhino.Geometry.Line`

        """
        lines = [self._get_args(self.primitive)]
        return compas_ghpython.draw_lines(lines)[0]

    @staticmethod
    def _get_args(primitive):
        start = list(primitive.start)
        end = list(primitive.end)
        return {'start': start, 'end': end}


# src/compas_ghpython/artists/meshartist.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import Rhino
from functools import partial

from compas.utilities import color_to_colordict

import compas_ghpython
from compas.artists import MeshArtist
from .artist import GHArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=False)


class MeshArtist(GHArtist, MeshArtist):
    """Artist for drawing mesh data structures.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A COMPAS mesh.
    show_vertices : bool, optional
        If True, draw the vertices of the mesh.
    show_edges : bool, optional
        If True, draw the edges of the mesh.
    show_faces : bool, optional
        If True, draw the faces of the mesh.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.MeshArtist` for more info.

    """

    def __init__(self,
                 mesh,
                 show_mesh=False,
                 show_vertices=True,
                 show_edges=True,
                 show_faces=True,
                 **kwargs):
        super(MeshArtist, self).__init__(mesh=mesh, **kwargs)
        self.show_mesh = show_mesh
        self.show_vertices = show_vertices
        self.show_edges = show_edges
        self.show_faces = show_faces

    def draw(self, vertices=None, edges=None, faces=None, vertexcolor=None, edgecolor=None, facecolor=None, color=None, join_faces=False):
        """Draw the mesh using the chosen visualization settings.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertices to draw.
            Default is None, in which case all vertices are drawn.
        edges : list[tuple[int, int]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        faces : list[int], optional
            A selection of faces to draw.
            The default is None, in which case all faces are drawn.
        vertexcolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color specification for the vertices.
            The default color is the value of :attr:`MeshArtist.default_vertexcolor`.
        edgecolor : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color specification for the edges.
            The default color is the value of :attr:`MeshArtist.default_edgecolor`.
        facecolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color specification for the faces.
            The default color is the value of :attr:`MeshArtist.default_facecolor`.
        color : tuple[int, int, int], optional
            The color of the mesh.
            Default is the value of :attr:`MeshArtist.default_color`.
        join_faces : bool, optional
            If True, join the individual faces into one mesh.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Mesh`, :rhino:`Rhino.Geometry.Point3d`, :rhino:`Rhino.Geometry.Line`]

        """
        geometry = []
        if self.show_mesh:
            geometry.append(self.draw_mesh(color=color))
        if self.show_vertices:
            geometry.extend(self.draw_vertices(vertices=vertices, color=vertexcolor))
        if self.show_edges:
            geometry.extend(self.draw_edges(edges=edges, color=edgecolor))
        if self.show_faces:
            geometry.extend(self.draw_faces(faces=faces, color=facecolor, join_faces=join_faces))
        return geometry

    def draw_mesh(self, color=None):
        """Draw the mesh as a RhinoMesh.

        This method is an alias for :attr:`MeshArtist.draw`.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The color of the mesh.
            Default is the value of :attr:`MeshArtist.default_color`.

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        Notes
        -----
        The mesh should be a valid Rhino Mesh object, which means it should have only triangular or quadrilateral faces.
        Faces with more than 4 vertices will be triangulated on-the-fly.

        """
        color = color or self.default_color
        vertices, faces = self.mesh.to_vertices_and_faces()
        return compas_ghpython.draw_mesh(vertices, faces, color)

    def draw_vertices(self, vertices=None, color=None):
        """Draw a selection of vertices.

        Parameters
        ----------
        vertices : list[int], optional
            A selection of vertices to draw.
            Default is None, in which case all vertices are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color specification for the vertices.
            The default is the value of :attr:`MeshArtist.default_vertexcolor`.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Point3d`]

        """
        self.vertex_color = color
        vertices = vertices or list(self.mesh.vertices())
        vertex_xyz = self.vertex_xyz
        points = []
        for vertex in vertices:
            points.append({
                'pos': vertex_xyz[vertex],
                'name': "{}.vertex.{}".format(self.mesh.name, vertex),
                'color': self.vertex_color.get(vertex, self.default_vertexcolor)
            })
        return compas_ghpython.draw_points(points)

    def draw_faces(self, faces=None, color=None, join_faces=False):
        """Draw a selection of faces.

        Parameters
        ----------
        faces : list[int], optional
            A selection of faces to draw.
            The default is None, in which case all faces are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color specification for the faces.
            The default color is the value of :attr:`MeshArtist.default_facecolor`.
        join_faces : bool, optional
            If True, join the individual faces into one mesh.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Mesh`]

        """
        self.face_color = color
        faces = faces or list(self.mesh.faces())
        vertex_xyz = self.vertex_xyz
        facets = []
        for face in faces:
            facets.append({
                'points': [vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)],
                'name': "{}.face.{}".format(self.mesh.name, face),
                'color': self.face_color.get(face, self.default_facecolor)
            })
        meshes = compas_ghpython.draw_faces(facets)
        if not join_faces:
            return meshes
        joined_mesh = Rhino.Geometry.Mesh()
        for mesh in meshes:
            joined_mesh.Append(mesh)
        return [joined_mesh]

    def draw_edges(self, edges=None, color=None):
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A selection of edges to draw.
            The default is None, in which case all edges are drawn.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color specification for the edges.
            The default color is the value of :attr:`MeshArtist.default_edgecolor`.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Line`]

        """
        self.edge_color = color
        edges = edges or list(self.mesh.edges())
        vertex_xyz = self.vertex_xyz
        lines = []
        for edge in edges:
            lines.append({
                'start': vertex_xyz[edge[0]],
                'end': vertex_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': "{}.edge.{}-{}".format(self.mesh.name, *edge)
            })
        return compas_ghpython.draw_lines(lines)

    def clear_edges(self):
        """GH Artists are state-less. Therefore, clear does not have any effect.

        Returns
        -------
        None

        """
        pass

    def clear_vertices(self):
        """GH Artists are state-less. Therefore, clear does not have any effect.

        Returns
        -------
        None

        """
        pass

    def clear_faces(self):
        """GH Artists are state-less. Therefore, clear does not have any effect.

        Returns
        -------
        None

        """
        pass


# src/compas_ghpython/artists/networkartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from functools import partial
import compas_ghpython

from compas.utilities import color_to_colordict

from compas.artists import NetworkArtist
from .artist import GHArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=False)


class NetworkArtist(GHArtist, NetworkArtist):
    """Artist for drawing network data structures.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A COMPAS network.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.NetworkArtist` for more info.

    """

    def __init__(self, network, **kwargs):
        super(NetworkArtist, self).__init__(network=network, **kwargs)

    def draw(self):
        """Draw the entire network with default color settings.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Point3d`]
            The objects representing the nodes of the network.
        list[:rhino:`Rhino.Geometry.Line`]
            The objects representing the edges of the network.

        """
        return self.draw_nodes(), self.draw_edges()

    def draw_nodes(self, nodes=None, color=None):
        """Draw a selection of nodes.

        Parameters
        ----------
        nodes: list[hashable], optional
            The selection of nodes that should be drawn.
            Default is None, in which case all nodes are drawn.
        color: tuple[int, int, int] or dict[hashable, tuple[int, int, int]], optional
            The color specification for the nodes.
            The default color is :attr:`NetworkArtist.default_nodecolor`.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Point3d`]

        """
        self.node_color = color
        node_xyz = self.node_xyz
        nodes = nodes or list(self.network.nodes())
        points = []
        for node in nodes:
            points.append({
                'pos': node_xyz[node],
                'name': "{}.node.{}".format(self.network.name, node),
                'color': self.node_color.get(node, self.default_nodecolor)
            })
        return compas_ghpython.draw_points(points)

    def draw_edges(self, edges=None, color=None):
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[hashable, hashable]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        color : tuple[int, int, int] or dict[tuple[hashable, hashable], tuple[int, int, int]], optional
            The color specification for the edges.
            The default color is the value of :attr:`NetworkArtist.default_edgecolor`.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Line`]

        """
        self.edge_color = color
        node_xyz = self.node_xyz
        edges = edges or list(self.network.edges())
        lines = []
        for edge in edges:
            lines.append({
                'start': node_xyz[edge[0]],
                'end': node_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': "{}.edge.{}-{}".format(self.network.name, *edge)
            })
        return compas_ghpython.draw_lines(lines)


# src/compas_ghpython/artists/pointartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython.utilities
from compas.artists import PrimitiveArtist
from .artist import GHArtist


class PointArtist(GHArtist, PrimitiveArtist):
    """Artist for drawing points.

    Parameters
    ----------
    point : :class:`compas.geometry.Point`
        A COMPAS point.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    """

    def __init__(self, point, **kwargs):
        super(PointArtist, self).__init__(primitive=point, **kwargs)

    def draw(self):
        """Draw the point.

        Returns
        -------
        :rhino:`Rhino.Geometry.Point3d`

        """
        points = [self._get_args(self.primitive)]
        return compas_ghpython.utilities.draw_points(points)[0]

    @staticmethod
    def _get_args(primitive):
        return {'pos': list(primitive)}


# src/compas_ghpython/artists/polygonartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import PrimitiveArtist
from .artist import GHArtist


class PolygonArtist(GHArtist, PrimitiveArtist):
    """Artist for drawing polygons.

    Parameters
    ----------
    polygon : :class:`compas.geometry.Polygon`
        A COMPAS polygon.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    """

    def __init__(self, polygon, **kwargs):
        super(PolygonArtist, self).__init__(primitive=polygon, **kwargs)

    def draw(self, show_points=False, show_edges=False, show_face=True):
        """Draw the polygon.

        Parameters
        ----------
        show_points : bool, optional
            If True, draw the points of the polygon.
        show_edges : bool, optional
            If True, draw the edges of the polygon.
        show_face : bool, optional
            If True, draw the face of the polygon.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Point3d`, :rhino:`Rhino.Geometry.Line`, :rhino:`Rhino.Geometry.Mesh`]
            The Rhino points, lines and face.

        """
        _points = map(list, self.primitive.points)
        result = []
        if show_points:
            points = [{'pos': point, 'color': self.color, 'name': self.primitive.name} for point in _points]
            result += compas_ghpython.draw_points(points)
        if show_edges:
            lines = [{'start': list(a), 'end': list(b), 'color': self.color, 'name': self.primitive.name} for a, b in self.primitive.lines]
            result += compas_ghpython.draw_lines(lines)
        if show_face:
            polygons = [{'points': _points, 'color': self.color, 'name': self.primitive.name}]
            result += compas_ghpython.draw_faces(polygons)
        return result


# src/compas_ghpython/artists/polyhedronartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import ShapeArtist
from .artist import GHArtist


class PolyhedronArtist(GHArtist, ShapeArtist):
    """Artist for drawing polyhedron shapes.

    Parameters
    ----------
    polyhedron : :class:`compas.geometry.Polyhedron`
        A COMPAS polyhedron.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.ShapeArtist` for more info.

    """

    def __init__(self, polyhedron, **kwargs):
        super(PolyhedronArtist, self).__init__(shape=polyhedron, **kwargs)

    def draw(self, color=None):
        """Draw the polyhedron associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the polyhedron.

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        color = color or self.color
        vertices = [list(vertex) for vertex in self.shape.vertices]
        faces = self.shape.faces
        mesh = compas_ghpython.draw_mesh(vertices,
                                         faces,
                                         color=color)
        return mesh


# src/compas_ghpython/artists/polylineartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import PrimitiveArtist
from .artist import GHArtist


class PolylineArtist(GHArtist, PrimitiveArtist):
    """Artist for drawing polylines.

    Parameters
    ----------
    polyline : :class:`compas.geometry.Polyline`
        A COMPAS polyline.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    """

    def __init__(self, polyline, **kwargs):
        super(PolylineArtist, self).__init__(primitive=polyline, **kwargs)

    def draw(self):
        """Draw the polyline.

        Returns
        -------
        :rhino:`Rhino.Geometry.Polyline`.

        """
        polylines = [self._get_args(self.primitive)]
        return compas_ghpython.draw_polylines(polylines)[0]

    @staticmethod
    def _get_args(primitive):
        return {'points': map(list, primitive.points)}


# src/compas_ghpython/artists/robotmodelartist.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import compas_ghpython
from compas.utilities import rgb_to_rgb

from compas_rhino.geometry.transformations import xtransform

from compas.artists import RobotModelArtist
from .artist import GHArtist


class RobotModelArtist(GHArtist, RobotModelArtist):
    """Artist for drawing robot models.

    Parameters
    ----------
    model : :class:`compas.robots.RobotModel`
        Robot model.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.RobotModelArtist` for more info.

    """

    def __init__(self, model, **kwargs):
        super(RobotModelArtist, self).__init__(model=model, **kwargs)

    # again not really sure why this is here
    def transform(self, native_mesh, transformation):
        xtransform(native_mesh, transformation)

    # same here
    # there is no reference to self...
    def create_geometry(self, geometry, name=None, color=None):
        if color:
            color = rgb_to_rgb(color[0], color[1], color[2])
        vertices, faces = geometry.to_vertices_and_faces()
        mesh = compas_ghpython.draw_mesh(vertices, faces, color=color)
        # Try to fix invalid meshes
        if not mesh.IsValid:
            mesh.FillHoles()
        return mesh

    def draw(self):
        """Draw the visual meshes of the robot model.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Mesh`]

        """
        return self.draw_visual()


# src/compas_ghpython/artists/sphereartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import ShapeArtist
from .artist import GHArtist


class SphereArtist(GHArtist, ShapeArtist):
    """Artist for drawing sphere shapes.

    Parameters
    ----------
    sphere : :class:`compas.geometry.Sphere`
        A COMPAS sphere.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.ShapeArtist` for more info.

    """

    def __init__(self, sphere, **kwargs):
        super(SphereArtist, self).__init__(shape=sphere, **kwargs)

    def draw(self, color=None, u=None, v=None):
        """Draw the sphere associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the sphere.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`SphereArtist.u`
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`SphereArtist.v`.

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        color = color or self.color
        u = u or self.u
        v = v or self.v
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        vertices = [list(vertex) for vertex in vertices]
        mesh = compas_ghpython.draw_mesh(vertices,
                                         faces,
                                         color=color)
        return mesh


# src/compas_ghpython/artists/torusartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_ghpython
from compas.artists import ShapeArtist
from .artist import GHArtist


class TorusArtist(GHArtist, ShapeArtist):
    """Artist for drawing torus shapes.

    Parameters
    ----------
    torus : :class:`compas.geometry.Torus`
        A COMPAS torus.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.ShapeArtist` for more info.

    """

    def __init__(self, torus, **kwargs):
        super(TorusArtist, self).__init__(shape=torus,  **kwargs)

    def draw(self, color=None, u=None, v=None):
        """Draw the torus associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the torus.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`TorusArtist.u`
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`TorusArtist.v`.

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        color = color or self.color
        u = u or self.u
        v = v or self.v
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        vertices = [list(vertex) for vertex in vertices]
        mesh = compas_ghpython.draw_mesh(vertices,
                                         faces,
                                         color=color)
        return mesh


# src/compas_ghpython/artists/vectorartist.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import compas_ghpython
from compas.artists import PrimitiveArtist
from compas.geometry import Point

from .artist import GHArtist


class VectorArtist(GHArtist, PrimitiveArtist):
    """Artist for drawing vectors.

    Parameters
    ----------
    vector : :class:`compas.geometry.Vector`
        A COMPAS vector.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    """

    def __init__(self, vector, **kwargs):
        super(VectorArtist, self).__init__(primitive=vector, **kwargs)

    def draw(self, point=None, show_point=False):
        """Draw the vector.

        Parameters
        ----------
        point : [float, float, float] or :class:`compas.geometry.Point`, optional
            Point of application of the vector.
            Default is ``Point(0, 0, 0)``.
        show_point : bool, optional
            If True, draw the point of application of the vector.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Point3d`, :rhino:`Rhino.Geometry.Line`]
            The Rhino line and endpoints, if requested.

        """
        if not point:
            point = [0, 0, 0]
        start = Point(*point)
        end = start + self.primitive
        start = list(start)
        end = list(end)
        result = []
        if show_point:
            points = [{'pos': start, 'color': self.color, 'name': self.primitive.name}]
            result += compas_ghpython.draw_points(points)
        lines = [{'start': start, 'end': end, 'arrow': 'end', 'color': self.color, 'name': self.primitive.name}]
        result += compas_ghpython.draw_lines(lines)
        return result


# src/compas_ghpython/artists/volmeshartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from functools import partial

import Rhino

from compas.utilities import color_to_colordict

import compas_ghpython
from compas.artists import VolMeshArtist
from .artist import GHArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=False)


class VolMeshArtist(GHArtist, VolMeshArtist):
    """Artist for drawing volmesh data structures.

    Parameters
    ----------
    volmesh : :class:`compas.datastructures.VolMesh`
        A COMPAS volmesh.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_ghpython.artists.GHArtist` and :class:`compas.artists.VolMeshArtist` for more info.

    """

    def __init__(self, volmesh, **kwargs):
        super(VolMeshArtist, self).__init__(volmesh=volmesh, **kwargs)

    def draw(self):
        raise NotImplementedError

    def draw_vertices(self, vertices=None, color=None):
        """Draw a selection of vertices.

        Parameters
        ----------
        vertices : list
            A list of vertices to draw.
            Default is None, in which case all vertices are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]]
            The color specification for the vertices.
            The default color of the vertices is :attr:`VolMeshArtist.default_vertexcolor`.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Point3d`]

        """
        self.vertex_color = color
        vertices = vertices or list(self.volmesh.vertices())
        vertex_xyz = self.vertex_xyz
        points = []
        for vertex in vertices:
            points.append({
                'pos': vertex_xyz[vertex],
                'name': "{}.vertex.{}".format(self.volmesh.name, vertex),
                'color': self.vertex_color.get(vertex, self.default_vertexcolor)
            })
        return compas_ghpython.draw_points(points)

    def draw_edges(self, edges=None, color=None):
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color specification for the edges.
            The default color is :attr:`VolMeshArtist.default_edgecolor`.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Line`]

        """
        self.edge_color = color
        edges = edges or list(self.volmesh.edges())
        vertex_xyz = self.vertex_xyz
        lines = []
        for edge in edges:
            lines.append({
                'start': vertex_xyz[edge[0]],
                'end': vertex_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': "{}.edge.{}-{}".format(self.volmesh.name, *edge)
            })
        return compas_ghpython.draw_lines(lines)

    def draw_faces(self, faces=None, color=None, join_faces=False):
        """Draw a selection of faces.

        Parameters
        ----------
        faces : list[list[int]], optional
            A list of faces to draw.
            The default is None, in which case all faces are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color specification for the faces.
            The default color is :attr:`VolMeshArtist.default_facecolor`.
        join_faces : bool, optional
            If True, join the faces into one mesh.

        Returns
        -------
        list[:rhino:`Rhino.Geometry.Mesh`]

        """
        self.face_color = color
        faces = faces or list(self.volmesh.faces())
        vertex_xyz = self.vertex_xyz
        facets = []
        for face in faces:
            facets.append({
                'points': [vertex_xyz[vertex] for vertex in self.volmesh.halfface_vertices(face)],
                'name': "{}.face.{}".format(self.volmesh.name, face),
                'color': self.face_color.get(face, self.default_facecolor)
            })
        meshes = compas_ghpython.draw_faces(facets)
        if not join_faces:
            return meshes
        joined_mesh = Rhino.Geometry.Mesh()
        for mesh in meshes:
            joined_mesh.Append(mesh)
        return [joined_mesh]


# src/compas_ghpython/components/__init__.py


"""
********************************************************************************
components
********************************************************************************

.. currentmodule:: compas_ghpython.components

.. rst-class:: lead

Utilities to work with Grasshopper components.

Installation
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    install_userobjects
    uninstall_userobjects

"""
from __future__ import absolute_import

import argparse
import glob
import os

from compas._os import create_symlinks
from compas._os import remove_symlinks
from compas_ghpython import get_grasshopper_userobjects_path
from compas_rhino import _check_rhino_version
import compas_rhino


def coerce_frame(plane):
    import Rhino
    from compas.geometry import Frame
    if isinstance(plane, Rhino.Geometry.Plane):
        return Frame(plane.Origin, plane.XAxis, plane.YAxis)
    elif isinstance(plane, Frame):
        return plane
    else:
        return Frame(*plane)


def get_version_from_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('-v', '--version', choices=compas_rhino.SUPPORTED_VERSIONS, default=compas_rhino.DEFAULT_VERSION)
    args = parser.parse_args()
    return _check_rhino_version(args.version)


def install_userobjects(source):
    """Installs Grasshopper user objects.

    Parameters
    ----------
    source : str
        Folder where the ghuser files to install are located.

    Returns
    -------
    list
        List of tuples (name, success) indicating whether each of the user objects was successfully installed.
    """
    version = get_version_from_args()

    # this dstdir potentially doesn't exist
    dstdir = get_grasshopper_userobjects_path(version)
    userobjects = glob.glob(os.path.join(source, '*.ghuser'))

    symlinks_to_remove = []
    symlinks_to_add = []
    for src in userobjects:
        dst = os.path.join(dstdir, os.path.basename(src))
        symlinks_to_remove.append(dst)
        symlinks_to_add.append((src, dst))

    # Remove existing links first
    remove_symlinks(symlinks_to_remove)

    # And the create new ones
    created = create_symlinks(symlinks_to_add)

    return list(zip(symlinks_to_add, created))


def uninstall_userobjects(userobjects):
    """
    Uninstalls Grasshopper user objects.

    Parameters
    ----------
    userobjects : list of str
        List of user object names to uninstall, eg. ``['Compas_Info.ghuser']``

    Returns
    -------
    list
        List of tuples (name, success) indicating whether each of the user objects was successfully removed.
    """
    version = get_version_from_args()
    dstdir = get_grasshopper_userobjects_path(version)

    userobjects = []
    for name in os.listdir(dstdir):
        if name.lower().startswith('compas'):
            userobjects.append(name)

    symlinks = []
    for obj in userobjects:
        path = os.path.join(dstdir, os.path.basename(obj))
        symlinks.append(path)

    removed = remove_symlinks(symlinks)

    return list(zip(symlinks, removed))


__all__ = [
    'install_userobjects',
    'uninstall_userobjects'
]


# src/compas_ghpython/utilities/__init__.py

"""
********************************************************************************
utilities
********************************************************************************

.. currentmodule:: compas_ghpython.utilities

This package contains utilities for working with COMPAS in Grasshopper.

drawing
=======

.. autosummary::
    :toctree: generated/

    draw_frame
    draw_points
    draw_lines
    draw_polylines
    draw_faces
    draw_cylinders
    draw_pipes
    draw_spheres
    draw_mesh
    draw_network


sets
====

.. autosummary::
    :toctree: generated/

    list_to_ghtree
    ghtree_to_list


timers
======

.. autosummary::
    :toctree: generated/

    update_component


misc
====

.. autosummary::
    :toctree: generated/

    unload_modules

"""
from __future__ import absolute_import

from compas_rhino.utilities import unload_modules
from .drawing import (
    draw_frame,
    draw_points,
    draw_lines,
    draw_geodesics,
    draw_polylines,
    draw_faces,
    draw_cylinders,
    draw_pipes,
    draw_spheres,
    draw_mesh,
    draw_network,
    draw_circles
)
from .sets import (
    list_to_ghtree,
    ghtree_to_list
)
from .timer import update_component

__all__ = [
    'unload_modules',
    'draw_frame',
    'draw_points',
    'draw_lines',
    'draw_geodesics',
    'draw_polylines',
    'draw_faces',
    'draw_cylinders',
    'draw_pipes',
    'draw_spheres',
    'draw_mesh',
    'draw_network',
    'draw_circles',
    'list_to_ghtree',
    'ghtree_to_list',
    'update_component'
]


# src/compas_ghpython/utilities/drawing.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from compas_rhino.utilities.drawing import _face_to_max_quad

from System.Enum import ToObject
from System.Array import CreateInstance
from System.Drawing import Color

import rhinoscriptsyntax as rs
import scriptcontext as sc

from compas.geometry import centroid_points
from compas.utilities import pairwise

from Rhino.Geometry import Point3d
from Rhino.Geometry import Vector3d
from Rhino.Geometry import Line
from Rhino.Geometry import Polyline
from Rhino.Geometry import Brep
from Rhino.Geometry import Cylinder
from Rhino.Geometry import Circle
from Rhino.Geometry import Plane
from Rhino.Geometry import PipeCapMode
from Rhino.Geometry import Curve
from Rhino.Geometry import Sphere
from Rhino.Geometry import Mesh
from Rhino.Geometry import Vector3f
from Rhino.Geometry import Point2f

try:
    from Rhino.Geometry import MeshNgon
except ImportError:
    MeshNgon = False

TOL = sc.doc.ModelAbsoluteTolerance


def draw_frame(frame):
    """Draw a frame.
    """
    pt = Point3d(*iter(frame.point))
    xaxis = Vector3d(*iter(frame.xaxis))
    yaxis = Vector3d(*iter(frame.yaxis))
    return Plane(pt, xaxis, yaxis)


def draw_points(points):
    """Draw points.

    Parameters
    ----------
    points : list of dict
        The point definitions.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Point3d`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'pos': lambda x: len(x) == 3)
        })

    """
    rg_points = []
    for p in iter(points):
        pos = p['pos']
        rg_points.append(Point3d(*pos))
    return rg_points


def draw_lines(lines):
    """Draw lines.

    Parameters
    ----------
    lines : list of dict
        The line definitions.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Line`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'start': lambda x: len(x) == 3),
            'end': lambda x: len(x) == 3),
        })

    """
    rg_lines = []
    for line in iter(lines):
        sp = line['start']
        ep = line['end']
        rg_lines.append(Line(Point3d(*sp), Point3d(*ep)))
    return rg_lines


def draw_geodesics(geodesics):
    """Draw geodesic lines on specified surfaces.

    Parameters
    ----------
    geodesics : list of dict
        The geodesic definitions.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Curve`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'start': lambda x: len(x) == 3),
            'end': lambda x: len(x) == 3),
            'srf': str
        })

    """
    rg_geodesics = []
    for g in iter(geodesics):
        sp = g['start']
        ep = g['end']
        srf = g['srf']
        curve = srf.ShortPath(Point3d(*sp), Point3d(*ep), TOL)
        rg_geodesics.append(curve)
    return rg_geodesics


def draw_polylines(polylines):
    """Draw polylines.

    Parameters
    ----------
    polylines : list of dict
        The polyline definitions.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Polyline`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'points': lambda x: all(len(y) == 3 for y in x)
        })

    """
    rg_polylines = []
    for p in iter(polylines):
        points = p['points']
        poly = Polyline([Point3d(*xyz) for xyz in points])
        poly.DeleteShortSegments(TOL)
        rg_polylines.append(poly)
    return rg_polylines


def draw_faces(faces):
    """Draw polygonal faces as Meshes.

    Parameters
    ----------
    faces : list of dict
        The face definitions.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Mesh`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'points': lambda x: all(len(y) == 3 for y in x),
            Optional('vertexcolors', default=None): lambda x: all(len(y) == 3 for y in x)
        })

    """
    meshes = []
    for face in iter(faces):
        points = face['points'][:]
        vertexcolors = face.get('vertexcolors')
        v = len(points)
        if v < 3:
            continue
        if v == 3:
            mfaces = [[0, 1, 2, 2]]
        elif v == 4:
            mfaces = [[0, 1, 2, 3]]
        else:
            mfaces = _face_to_max_quad(points, range(v))
            if vertexcolors:
                r, g, b = [sum(component) / v for component in zip(*vertexcolors)]
                r = int(min(max(0, r), 255))
                g = int(min(max(0, g), 255))
                b = int(min(max(0, b), 255))
                vertexcolors.append((r, g, b))
        if vertexcolors:
            mesh = draw_mesh(points, mfaces, color=vertexcolors)
        else:
            mesh = draw_mesh(points, mfaces)
        meshes.append(mesh)
    return meshes


def draw_cylinders(cylinders, cap=False):
    """Draw cylinders.

    Parameters
    ----------
    cylinders : list of dict
        The cylinder definitions.

    Other Parameters
    ----------------
    cap : bool, optional
        Default is False.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Cylinder`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'start': lambda x: len(x) == 3,
            'end': lambda x: len(x) == 3,
            'radius': And(Or(int, float), lambda x: x > 0)
        })

    """
    rg_cylinders = []
    for c in iter(cylinders):
        start = c['start']
        end = c['end']
        radius = c['radius']
        if radius < TOL:
            continue
        base = Point3d(*start)
        normal = Point3d(*end) - base
        height = normal.Length
        if height < TOL:
            continue
        plane = Plane(base, normal)
        circle = Circle(plane, radius)
        cylinder = Cylinder(circle, height)
        brep = cylinder.ToBrep(cap, cap)
        if not brep:
            continue
        rg_cylinders.append(brep)
    return rg_cylinders


def draw_pipes(pipes, cap=2, fit=1.0):
    """Draw pipes.

    Parameters
    ----------
    pipes : list of dict
        The pipe definitions.

    Other Parameters
    ----------------
    cap : {0, 1, 2}, optional
    fit : float, optional

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Brep`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'points': lambda x: all(len(y) == 3 for y in x),
            'radius': And(Or(int, float), lambda x: x > 0)
        })

    """
    abs_tol = TOL
    ang_tol = sc.doc.ModelAngleToleranceRadians
    for p in pipes:
        points = p['points']
        radius = p['radius']
        params = [0.0, 1.0]
        cap = ToObject(PipeCapMode, cap)
        if type(radius) in (int, float):
            radius = [radius] * 2
        radius = [float(r) for r in radius]

        rail = Curve.CreateControlPointCurve([Point3d(*xyz) for xyz in points])
        breps = Brep.CreatePipe(rail, params, radius, 1, cap, fit, abs_tol,
                                ang_tol)
        for brep in breps:
            yield brep


def draw_spheres(spheres):
    """Draw spheres.

    Parameters
    ----------
    spheres : list of dict
        The sphere definitions.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Sphere`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'pos': lambda x: len(x) == 3,
            'radius': And(Or(int, float), lambda x: x > 0)
        })

    """
    rg_sheres = []
    for s in iter(spheres):
        pos = s['pos']
        radius = s['radius']
        rg_sheres.append(Sphere(Point3d(*pos), radius))
    return rg_sheres


def draw_mesh(vertices, faces, color=None, vertex_normals=None, texture_coordinates=None):
    """Draw mesh in Grasshopper.

    Parameters
    ----------
    vertices : list of point
        List of vertex locations.
    faces : list of list of int
        List of faces defined as lists of indices into the list of vertices.

    Other Parameters
    ----------------
    color : tuple, list or :class:`System.Drawing.Color`, optional
    vertex_normals : bool, optional
    texture_coordinates

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Mesh`]

    """
    mesh = Mesh()
    for a, b, c in vertices:
        mesh.Vertices.Add(a, b, c)
    for face in faces:
        f = len(face)
        if f < 3:
            continue
        if f == 3:
            mesh.Faces.AddFace(*face)
        elif f == 4:
            mesh.Faces.AddFace(*face)
        else:
            if MeshNgon:
                centroid = centroid_points([vertices[index] for index in face])
                c = mesh.Vertices.Add(*centroid)
                facets = []
                for i, j in pairwise(face + face[:1]):
                    facets.append(mesh.Faces.AddFace(i, j, c))
                ngon = MeshNgon.Create(face, facets)
                mesh.Ngons.AddNgon(ngon)

    if vertex_normals:
        count = len(vertex_normals)
        normals = CreateInstance(Vector3f, count)
        for i, normal in enumerate(vertex_normals):
            normals[i] = Vector3f(normal[0], normal[1], normal[2])
        mesh.Normals.SetNormals(normals)

    if texture_coordinates:
        count = len(texture_coordinates)
        tcs = CreateInstance(Point2f, count)
        for i, tc in enumerate(texture_coordinates):
            tcs[i] = Point2f(tc[0], tc[1])
        mesh.TextureCoordinates.SetTextureCoordinates(tcs)

    if color:
        count = len(mesh.Vertices)
        colors = CreateInstance(Color, count)
        for i in range(count):
            colors[i] = rs.coercecolor(color)
        mesh.VertexColors.SetColors(colors)

    return mesh


def draw_network(network):
    """Draw a network in Grasshopper.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`

    Returns
    -------
    tuple
        A list[:rhino:`Rhino.Geometry.Point3d`].
        A list[:rhino:`Rhino.Geometry.Line`].

    """
    points = []
    for key in network.nodes():
        points.append({
            'pos': network.node_coordinates(key)})
    lines = []
    for u, v in network.edges():
        lines.append({
            'start': network.node_coordinates(u),
            'end': network.node_coordinates(v)})
    points_rg = draw_points(points)
    lines_rg = draw_lines(lines)

    return points_rg, lines_rg


def draw_circles(circles):
    """Draw circles in Grasshopper.

    Parameters
    ----------
    circles : list of dict
        The circle definitions.

    Returns
    -------
    list[:rhino:`Rhino.Geometry.Circle`]

    Notes
    -----
    .. code-block:: python

        Schema({
            'plane': lambda x: len(x[0]) == 3 and len(x[1]) == 3,
            'radius': And(Or(int, float), lambda x: x > 0)
        })

    """
    rg_circles = []
    for c in iter(circles):
        point, normal = c['plane']
        radius = c['radius']
        rg_circles.append(Circle(Plane(Point3d(*point), Vector3d(*normal)), radius))
    return rg_circles


# src/compas_ghpython/utilities/sets.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from System import Array

try:
    from Grasshopper import DataTree as Tree
    from Grasshopper.Kernel.Data import GH_Path as Path
except ImportError:
    pass


def list_to_ghtree(items, none_and_holes=False, base_path=[0]):
    """Transforms nestings of lists or tuples to a Grasshopper DataTree.

    Parameters
    ----------
    items : list
        Nesting of lists and/or tuples.
    none_and_holes : bool, optional
    base_path : list, optional

    Examples
    --------
    >>> items = [[1, 2], 3, [], [4, [5]]]
    >>> a = list_to_tree(items)
    >>> b = list_to_tree(items, none_and_holes=True, base_path=[7, 1])

    """
    def process_one_item(items, tree, track):
        path = Path(Array[int](track))
        if len(items) == 0 and none_and_holes:
            tree.EnsurePath(path)
            return
        for i, item in enumerate(items):
            if hasattr(item, '__iter__'):
                track.append(i)
                process_one_item(item, tree, track)
                track.pop()
            else:
                if none_and_holes:
                    tree.Insert(item, path, i)
                elif item is not None:
                    tree.Add(item, path)
    tree = Tree[object]()
    if items is not None:
        process_one_item(items, tree, base_path[:])
    return tree


def ghtree_to_list(tree):
    """Returns a list representation of a Grasshopper DataTree

    Parameters
    ----------
    tree : :class:`Grasshopper.DataTree`

    Returns
    -------
    list

    Examples
    --------
    >>> tree = Tree[object]()
    >>> [tree.Add(str("entry: " + str(i)), Path(Array[int]([i]))) for i in range(3)]
    >>> items = ghtree_to_list(tree)

    """
    def extend_at(path, index, simple_input, rest_list):
        target = path[index]
        if len(rest_list) <= target:
            rest_list.extend([None] * (target - len(rest_list) + 1))
        if index == path.Length - 1:
            rest_list[target] = list(simple_input)
        else:
            if rest_list[target] is None:
                rest_list[target] = []
            extend_at(path, index + 1, simple_input, rest_list[target])
    items = []
    for i in range(tree.BranchCount):
        path = tree.Path(i)
        extend_at(path, 0, tree.Branch(path), items)
    return items


# src/compas_ghpython/utilities/timer.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

try:
    import Grasshopper as gh
except ImportError:
    pass


def update_component(ghenv, delay):
    """Schedule an update of the Grasshopper component.

    After the specified delay, the GH component will be automatically updated.

    Parameters
    ----------
    ghenv : :class:`GhPython.Component.PythonEnvironment`
        The current GHPython environment.
    delay : :obj:`int`
        Time in milliseconds until the update is performed.

    Raises
    ------
    ValueError
        If the delay is less than zero.

    """
    if delay <= 0:
        raise ValueError('Delay must be greater than zero')

    ghcomp = ghenv.Component
    ghdoc = ghcomp.OnPingDocument()

    def callback(ghdoc):
        if ghdoc.SolutionState != gh.Kernel.GH_ProcessStep.Process:
            ghcomp.ExpireSolution(False)

    ghdoc.ScheduleSolution(
        delay, gh.Kernel.GH_Document.GH_ScheduleDelegate(callback))


# src/compas_plotters/artists/__init__.py

"""
********************************************************************************
artists
********************************************************************************

.. currentmodule:: compas_plotters.artists


Primitive Artists
=================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    PointArtist
    VectorArtist
    LineArtist
    PolylineArtist
    PolygonArtist
    CircleArtist
    EllipseArtist


Datastructure Artists
=====================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    MeshArtist
    NetworkArtist


Base Classes
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    PlotterArtist

"""

from compas.plugins import plugin
from compas.artists import Artist

from compas.geometry import Point
from compas.geometry import Vector
from compas.geometry import Line
from compas.geometry import Polyline
from compas.geometry import Polygon
from compas.geometry import Circle
from compas.geometry import Ellipse

from compas.datastructures import Mesh
from compas.datastructures import Network

from .artist import PlotterArtist
from .pointartist import PointArtist
from .vectorartist import VectorArtist
from .lineartist import LineArtist
from .polylineartist import PolylineArtist
from .polygonartist import PolygonArtist
from .circleartist import CircleArtist
from .ellipseartist import EllipseArtist
from .meshartist import MeshArtist
from .networkartist import NetworkArtist


@plugin(category='factories', requires=['matplotlib'])
def register_artists():
    Artist.register(Point, PointArtist, context='Plotter')
    Artist.register(Vector, VectorArtist, context='Plotter')
    Artist.register(Line, LineArtist, context='Plotter')
    Artist.register(Polyline, PolylineArtist, context='Plotter')
    Artist.register(Polygon, PolygonArtist, context='Plotter')
    Artist.register(Circle, CircleArtist, context='Plotter')
    Artist.register(Ellipse, EllipseArtist, context='Plotter')
    Artist.register(Mesh, MeshArtist, context='Plotter')
    Artist.register(Network, NetworkArtist, context='Plotter')


__all__ = [
    'PlotterArtist',
    'PointArtist',
    'VectorArtist',
    'LineArtist',
    'PolylineArtist',
    'PolygonArtist',
    'CircleArtist',
    'EllipseArtist',
    'MeshArtist',
    'NetworkArtist',
]


# src/compas_plotters/artists/artist.py

from abc import abstractproperty

from compas.artists import Artist


class PlotterArtist(Artist):
    """Base class for all plotter artists.

    Attributes
    ----------
    plotter : :class:`compas_plotters.plotter.Plotter`, read-only
        A plotter instance.
    data : list[[float, float]]
        The geometrical data points visualized with the plotter.

    """

    def __init__(self, plotter, **kwargs):
        super().__init__(**kwargs)
        self._plotter = plotter

    @property
    def plotter(self):
        # if not self._plotter:
        #     from compas_plotters import Plotter
        #     self._plotter = Plotter()
        return self._plotter

    @abstractproperty
    def data(self):
        raise NotImplementedError

    def viewbox(self):
        """Compute the bounds of the current view.

        Returns
        -------
        tuple[[float, float], [float, float], [float, float], [float]]
            Coordinates of the corners of the 2D view box.

        """
        xlim = self.plotter.axes.get_xlim()
        ylim = self.plotter.axes.get_ylim()
        xmin, xmax = xlim
        ymin, ymax = ylim
        return [xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]

    def update_data(self) -> None:
        """Update the data limits of the plotting axes using the visualization data.

        Returns
        -------
        None

        """
        self.plotter.axes.update_datalim(self.data)


# src/compas_plotters/artists/circleartist.py

from typing import Tuple
from typing import List
from typing import Any
from typing_extensions import Literal

from matplotlib.patches import Circle as CirclePatch
from compas.geometry import Circle

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class CircleArtist(PlotterArtist, PrimitiveArtist):
    """Artist for COMPAS circles.

    Parameters
    ----------
    circle : :class:`compas.geometry.Circle`
        A COMPAS circle.
    linewidth : float, optional
        Width of the circle boundary.
    linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional
        Style of the circle boundary.
    facecolor : tuple[float ,float, float], optional
        Color of the interior of the circle.
    edgecolor : tuple[float, float, float], optional
        Color of the boundary of the circle.
    fill : bool, optional
        If True, draw the interior of the circle.
    alpha : float, optional
        Transparency of the circle.
    zorder : int, optional
        Stacking order of the circle on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    circle : :class:`compas.geometry.Circle`
        The circle associated with the artist.

    """

    def __init__(self,
                 circle: Circle,
                 linewidth: float = 1.0,
                 linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid',
                 facecolor: Color = (1.0, 1.0, 1.0),
                 edgecolor: Color = (0, 0, 0),
                 fill: bool = True,
                 alpha: float = 1.0,
                 zorder: int = 1000,
                 **kwargs: Any):

        super().__init__(primitive=circle, **kwargs)

        self._mpl_circle = None
        self.linewidth = linewidth
        self.linestyle = linestyle
        self.facecolor = facecolor
        self.edgecolor = edgecolor
        self.fill = fill
        self.alpha = alpha
        self.zorder = zorder

    @property
    def circle(self):
        return self.primitive

    @circle.setter
    def circle(self, circle):
        self.primitive = circle

    @property
    def data(self) -> List[List[float]]:
        points = [
            self.circle.center[:2],
            self.circle.center[:2],
            self.circle.center[:2],
            self.circle.center[:2]
        ]
        points[0][0] -= self.circle.radius
        points[1][0] += self.circle.radius
        points[2][1] -= self.circle.radius
        points[3][1] += self.circle.radius
        return points

    def draw(self) -> None:
        """Draw the circle on the plotter canvas.

        Returns
        -------
        None

        """
        circle = CirclePatch(
            self.circle.center[:2],
            linewidth=self.linewidth,
            linestyle=self.linestyle,
            radius=self.circle.radius,
            facecolor=self.facecolor,
            edgecolor=self.edgecolor,
            fill=self.fill,
            alpha=self.alpha,
            zorder=self.zorder
        )
        self._mpl_circle = self.plotter.axes.add_artist(circle)
        self.update_data()

    def redraw(self) -> None:
        """Update the circle using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        self._mpl_circle.center = self.circle.center[:2]
        self._mpl_circle.set_radius(self.circle.radius)
        self._mpl_circle.set_edgecolor(self.edgecolor)
        self._mpl_circle.set_facecolor(self.facecolor)
        self.update_data()


# src/compas_plotters/artists/ellipseartist.py

from typing import Tuple
from typing import List
from typing import Any
from typing_extensions import Literal

from matplotlib.patches import Ellipse as EllipsePatch
from compas.geometry import Ellipse

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class EllipseArtist(PlotterArtist, PrimitiveArtist):
    """Artist for COMPAS ellipses.

    Parameters
    ----------
    ellipse : :class:`compas.geometry.Ellipse`
        A COMPAS ellipse.
    linewidth : float, optional
        Width of the ellipse boundary.
    linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional
        Style of the ellipse boundary.
    facecolor : tuple[float ,float, float], optional
        Color of the interior of the ellipse.
    edgecolor : tuple[float, float, float], optional
        Color of the boundary of the ellipse.
    fill : bool, optional
        If True, draw the interior of the ellipse.
    alpha : float, optional
        Transparency of the ellipse.
    zorder : int, optional
        Stacking order of the ellipse on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    ellipse : :class:`compas.geometry.Ellipse`
        The ellipse associated with the artist.

    """

    def __init__(self,
                 ellipse: Ellipse,
                 linewidth: float = 1.0,
                 linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid',
                 facecolor: Color = (1.0, 1.0, 1.0),
                 edgecolor: Color = (0, 0, 0),
                 fill: bool = True,
                 alpha: float = 1.0,
                 zorder: int = 1000,
                 **kwargs: Any):

        super().__init__(primitive=ellipse, **kwargs)

        self._mpl_ellipse = None
        self.linewidth = linewidth
        self.linestyle = linestyle
        self.facecolor = facecolor
        self.edgecolor = edgecolor
        self.fill = fill
        self.alpha = alpha
        self.zorder = zorder

    @property
    def ellipse(self):
        return self.primitive

    @ellipse.setter
    def ellipse(self, ellipse):
        self.primitive = ellipse

    @property
    def data(self) -> List[List[float]]:
        points = [
            self.ellipse.center[:2],
            self.ellipse.center[:2],
            self.ellipse.center[:2],
            self.ellipse.center[:2]
        ]
        points[0][0] -= self.ellipse.major
        points[1][0] += self.ellipse.major
        points[2][1] -= self.ellipse.minor
        points[3][1] += self.ellipse.minor
        return points

    def draw(self) -> None:
        """Draw the ellipse on the plotter canvas.

        Returns
        -------
        None

        """
        ellipse = EllipsePatch(
            self.ellipse.center[:2],
            width=2*self.ellipse.major,
            height=2*self.ellipse.minor,
            facecolor=self.facecolor,
            edgecolor=self.edgecolor,
            fill=self.fill,
            alpha=self.alpha,
            zorder=self.zorder)
        self._mpl_ellipse = self.plotter.axes.add_artist(ellipse)

    def redraw(self) -> None:
        """Update the ellipse using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        self._mpl_ellipse.center = self.ellipse.center[:2]
        self._mpl_ellipse.set_width(2*self.ellipse.major)
        self._mpl_ellipse.set_height(2*self.ellipse.minor)
        self._mpl_ellipse.set_edgecolor(self.edgecolor)
        self._mpl_ellipse.set_facecolor(self.facecolor)


# src/compas_plotters/artists/lineartist.py

from typing import Tuple
from typing import List
from typing import Any
from typing_extensions import Literal

from matplotlib.lines import Line2D
from compas.geometry import Point, Line
from compas.geometry import intersection_line_box_xy

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class LineArtist(PlotterArtist, PrimitiveArtist):
    """Artist for COMPAS lines.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`
        A COMPAS line.
    draw_points : bool, optional
        If True, draw the start and end point of the line.
    draw_as_segment : bool, optional
        If True, draw only the segment between start and end, instead of the infinite line.
    linewidth : float, optional
        Width of the line.
    linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional
        Style of the line.
    color : tuple[float, float, float], optional
        Color of the line.
    zorder : int, optional
        Stacking order of the line on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    line : :class:`compas.geometry.Line`
        The line associated with the artist.

    """

    def __init__(self,
                 line: Line,
                 draw_points: bool = False,
                 draw_as_segment: bool = False,
                 linewidth: float = 1.0,
                 linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid',
                 color: Color = (0, 0, 0),
                 zorder: int = 1000,
                 **kwargs: Any):

        super().__init__(primitive=line, **kwargs)

        self._mpl_line = None
        self._start_artist = None
        self._end_artist = None
        self._segment_artist = None
        self.draw_points = draw_points
        self.draw_as_segment = draw_as_segment
        self.linewidth = linewidth
        self.linestyle = linestyle
        self.color = color
        self.zorder = zorder

    @property
    def line(self):
        return self.primitive

    @line.setter
    def line(self, line):
        self.primitive = line

    @property
    def data(self) -> List[List[float]]:
        return [self.line.start[:2], self.line.end[:2]]

    def clip(self) -> List[Point]:
        """Compute the clipping points of the line for the current view box.

        Returns
        -------
        list[[float, float, float]]
            The intersection between the line and the viewbox.

        """
        xlim, ylim = self.plotter.viewbox
        xmin, xmax = xlim
        ymin, ymax = ylim
        box = [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]]
        return intersection_line_box_xy(self.line, box)

    def draw(self) -> None:
        """Draw the line associated with the artist.

        Returns
        -------
        None

        """
        if self.draw_as_segment:
            x0, y0 = self.line.start[:2]
            x1, y1 = self.line.end[:2]
            line2d = Line2D([x0, x1], [y0, y1],
                            linewidth=self.linewidth,
                            linestyle=self.linestyle,
                            color=self.color,
                            zorder=self.zorder)
            self._mpl_line = self.plotter.axes.add_line(line2d)
            if self.draw_points:
                self._start_artist = self.plotter.add(self.line.start)
                self._end_artist = self.plotter.add(self.line.end)
        else:
            points = self.clip()
            if points:
                p0, p1 = points
                x0, y0 = p0[:2]
                x1, y1 = p1[:2]
                line2d = Line2D([x0, x1], [y0, y1],
                                linewidth=self.linewidth,
                                linestyle=self.linestyle,
                                color=self.color,
                                zorder=self.zorder)
                self._mpl_line = self.plotter.axes.add_line(line2d)
                if self.draw_points:
                    self._start_artist = self.plotter.add(self.line.start, edgecolor=self.color)
                    self._end_artist = self.plotter.add(self.line.end, edgecolor=self.color)

    def redraw(self) -> None:
        """Update the line using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        if self.draw_as_segment:
            x0, y0 = self.line.start[:2]
            x1, y1 = self.line.end[:2]
            self._mpl_line.set_xdata([x0, x1])
            self._mpl_line.set_ydata([y0, y1])
            self._mpl_line.set_color(self.color)
            self._mpl_line.set_linewidth(self.linewidth)
        else:
            points = self.clip()
            if points:
                p0, p1 = points
                x0, y0 = p0[:2]
                x1, y1 = p1[:2]
                self._mpl_line.set_xdata([x0, x1])
                self._mpl_line.set_ydata([y0, y1])
                self._mpl_line.set_color(self.color)
                self._mpl_line.set_linewidth(self.linewidth)
        if self.draw_points:
            self._start_artist.redraw()
            self._end_artist.redraw()


# src/compas_plotters/artists/meshartist.py

from typing import Dict
from typing import Tuple
from typing import List
from typing import Union
from typing import Optional
from typing import Any

from typing_extensions import Literal

from matplotlib.collections import LineCollection, PatchCollection
from matplotlib.patches import Polygon as PolygonPatch
from matplotlib.patches import Circle

from compas.geometry import centroid_points_xy
from compas.geometry import Line
from compas.geometry import offset_line
from compas.geometry import Frame
from compas.geometry import Scale
from compas.datastructures import Mesh
from compas.artists import MeshArtist
from compas.utilities import is_color_rgb
from compas.utilities.colors import is_color_light

from .artist import PlotterArtist

Color = Tuple[float, float, float]


class MeshArtist(PlotterArtist, MeshArtist):
    """Artist for COMPAS mesh data structures.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A COMPAS mesh.
    vertices : list[int], optional
        Selection of vertex identifiers.
        Default is None, in which case all vertices are drawn.
    edges : list[tuple[int, int]], optional
        Selection of edge identifiers.
        The default is None, in which case all edges are drawn.
    faces : list[int], optional
        Selection of face identifiers.
        The default is None, in which case all faces are drawn.
    vertexcolor : tuple[float, float, float] or dict[int, tuple[float, float, float]], optional
        Color specification for the vertices.
    edgecolor : tuple[float, float, float] or dict[tuple[int, int], tuple[float, float, float]], optional
        Color specification for the edges.
    facecolor : tuple[float, float, float] or dict[int, tuple[float, float, float]], optional
        Color specification for the faces.
    show_vertices : bool, optional
        If True, draw the vertices of the mesh.
    show_edges : bool, optional
        If True, draw the edges of the mesh.
    show_faces : bool, optional
        If True, draw the faces of the mesh.
    vertexsize : int, optional
        Size of the vertices.
    vertextext : str or dict[int, str], optional
        Labels for the vertices.
    edgetext : str or dict[tuple[int, int], str], optional
        Labels for the edges.
    facetext : str or dict[int, str], optional
        Labels for the faces.
    sizepolicy : {'relative', 'absolute'}, optional
        The policy for sizing the vertices.
        If ``'relative'``, the value of `vertexsize` is scaled by the number of vertices.
        If ``'absolute'``, the value of `vertexsize` is scaled by the resolution of the plotter (:attr:MeshArtist.plotter.dpi).
    zorder : int, optional
        The base stacking order of the components of the mesh on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.MeshArtist` for more info.

    Attributes
    ----------
    halfedges : list[tuple[int, int]]
        The halfedges to include in the drawing.
    vertex_size : dict[int, float]
        Mapping between vertex identifiers and vertex sizes.
    halfedge_color : dict[tuple[int, int], tuple[float, float, float]]
        Mapping between halfedge identifiers and halfedge colors.
    zorder_faces : int, read-only
        The stacking order of the faces relative to the base stacking order of the mesh.
    zorder_edges : int, read-only
        The stacking order of the edges relative to the base stacking order of the mesh.
    zorder_vertices : int, read-only
        The stacking order of the vertices relative to the base stacking order of the mesh.

    """

    default_halfedgecolor = (0.7, 0.7, 0.7)

    def __init__(self,
                 mesh: Mesh,
                 vertices: Optional[List[int]] = None,
                 edges: Optional[List[int]] = None,
                 faces: Optional[List[int]] = None,
                 vertexcolor: Color = (1.0, 1.0, 1.0),
                 edgecolor: Color = (0.0, 0.0, 0.0),
                 facecolor: Color = (0.9, 0.9, 0.9),
                 edgewidth: float = 1.0,
                 show_vertices: bool = True,
                 show_edges: bool = True,
                 show_faces: bool = True,
                 vertexsize: int = 5,
                 vertextext: Optional[Union[str, Dict[int, str]]] = None,
                 edgetext: Optional[Union[str, Dict[Tuple[int, int], str]]] = None,
                 facetext: Optional[Union[str, Dict[int, str]]] = None,
                 sizepolicy: Literal['relative', 'absolute'] = 'relative',
                 zorder: int = 1000,
                 **kwargs: Any):

        super().__init__(mesh=mesh, **kwargs)

        self.sizepolicy = sizepolicy
        self.vertices = vertices
        self.edges = edges
        self.faces = faces
        self.vertex_color = vertexcolor
        self.vertex_size = vertexsize
        self.vertex_text = vertextext
        self.edge_color = edgecolor
        self.edge_width = edgewidth
        self.face_color = facecolor
        self.show_vertices = show_vertices
        self.show_edges = show_edges
        self.show_faces = show_faces
        self.zorder = zorder

        self._halfedges = None
        self._halfedgecollection = None
        self._halfedge_color = None

    @property
    def halfedges(self):
        if not self._halfedges:
            self._halfedges = [(u, v) for u in self.mesh.halfedge for v in self.mesh.halfedge[u]]
        return self._halfedges

    @halfedges.setter
    def halfedges(self, halfedges):
        self._halfedges = halfedges

    @property
    def vertex_size(self):
        if not self._vertex_size:
            factor = self.plotter.dpi if self.sizepolicy == 'absolute' else self.mesh.number_of_vertices()
            size = self.default_vertexsize / factor
            self._vertex_size = {vertex: size for vertex in self.mesh.vertices()}
        return self._vertex_size

    @vertex_size.setter
    def vertex_size(self, vertexsize):
        factor = self.plotter.dpi if self.sizepolicy == 'absolute' else self.mesh.number_of_vertices()
        if isinstance(vertexsize, dict):
            self.vertex_size.update({vertex: size / factor for vertex, size in vertexsize.items()})
        elif isinstance(vertexsize, (int, float)):
            self._vertex_size = {vertex: vertexsize / factor for vertex in self.mesh.vertices()}

    @property
    def halfedge_color(self):
        if self._halfedge_color is None:
            self._halfedge_color = {(u, v): self.default_halfedgecolor for u in self.mesh.halfedge for v in self.mesh.halfedge[u]}
        return self._halfedge_color

    @halfedge_color.setter
    def halfedge_color(self, halfedge_color):
        if isinstance(halfedge_color, dict):
            self._halfedge_color = halfedge_color
        elif is_color_rgb(halfedge_color):
            self._halfedge_color = {(u, v): halfedge_color for u in self.mesh.halfedge for v in self.mesh.halfedge[u]}

    @property
    def zorder_faces(self):
        return self.zorder + 10

    @property
    def zorder_edges(self):
        return self.zorder + 20

    @property
    def zorder_vertices(self):
        return self.zorder + 30

    @property
    def item(self):
        """Mesh: Alias for ``~MeshArtist.mesh``"""
        return self.mesh

    @item.setter
    def item(self, item: Mesh):
        self.mesh = item

    @property
    def data(self) -> List[List[float]]:
        return self.mesh.vertices_attributes('xy')

    # ==============================================================================
    # clear and draw
    # ==============================================================================

    def clear_vertices(self) -> None:
        """Clear the current vertices from the canvas.

        Returns
        -------
        None

        """
        if self._vertexcollection:
            self._vertexcollection.remove()
        self._vertexcollection = None

    def clear_edges(self) -> None:
        """Clear the current edges from the canvas.

        Returns
        -------
        None

        """
        if self._edgecollection:
            self._edgecollection.remove()
        self._edgecollection = None

    def clear_halfedges(self) -> None:
        """Clear the current halfedges from the canvas.

        Returns
        -------
        None

        """
        if self._halfedgecollection:
            for artist in self._halfedgecollection:
                artist.remove()
        self._halfedgecollection = None

    def clear_faces(self) -> None:
        """Clear the current faces from the canvas.

        Returns
        -------
        None

        """
        if self._facecollection:
            self._facecollection.remove()
        self._facecollection = None

    def draw(self,
             vertices: Optional[List[int]] = None,
             edges: Optional[List[Tuple[int, int]]] = None,
             faces: Optional[List[int]] = None,
             vertexcolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None,
             edgecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None,
             facecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None
             ) -> None:
        """Draw the mesh.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertex identifiers.
            Default is None, in which case all vertices are drawn.
        edges : list[tuple[int, int]], optional
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        faces : list[int], optional
            A list of face identifiers.
            The default is None, in which case all faces are drawn.
        vertexcolor : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification for the vertices.
        edgecolor : rgb-tuple or dict[tuple[int, int], rgb-tuple], optional
            The color specification for the edges.
        facecolor : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification for the faces.

        Returns
        -------
        None

        """
        self.clear()
        if self.show_vertices:
            self.draw_vertices(vertices=vertices, color=vertexcolor)
        if self.show_edges:
            self.draw_edges(edges=edges, color=edgecolor)
        if self.show_faces:
            self.draw_faces(faces=faces, color=facecolor)

    def draw_mesh(self):
        raise NotImplementedError

    def draw_vertices(self,
                      vertices: Optional[List[int]] = None,
                      color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None
                      ) -> None:
        """Draw a selection of vertices.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertex identifiers.
            Default is None, in which case all vertices are drawn.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification for the vertices.

        Returns
        -------
        None

        """
        self.clear_vertices()

        if vertices:
            self.vertices = vertices
        if color:
            self.vertex_color = color

        circles = []
        for vertex in self.vertices:
            x, y = self.vertex_xyz[vertex][:2]
            circle = Circle(
                [x, y],
                radius=self.vertex_size.get(vertex, self.default_vertexsize),
                facecolor=self.vertex_color.get(vertex, self.default_vertexcolor),
                edgecolor=(0, 0, 0),
                lw=0.3,
            )
            circles.append(circle)

        collection = PatchCollection(
            circles,
            match_original=True,
            zorder=self.zorder_vertices,
            alpha=1.0,
            picker=5
        )
        self.plotter.axes.add_collection(collection)
        self._vertexcollection = collection

    def draw_edges(self,
                   edges: Optional[List[Tuple[int, int]]] = None,
                   color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None
                   ) -> None:
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        color : rgb-tuple or dict[tuple[int, int], rgb-tuple], optional
            The color specification for the edges.

        Returns
        -------
        None

        """
        self.clear_edges()
        if edges:
            self.edges = edges
        if color:
            self.edge_color = color

        lines = []
        colors = []
        widths = []
        for edge in self.edges:
            u, v = edge
            lines.append([self.vertex_xyz[edge[0]][:2], self.vertex_xyz[edge[1]][:2]])
            colors.append(self.edge_color.get(edge, self.edge_color.get((v, u), self.default_edgecolor)))
            widths.append(self.edge_width.get(edge, self.edge_width.get((v, u), self.default_edgewidth)))

        collection = LineCollection(
            lines,
            linewidths=widths,
            colors=colors,
            linestyle='solid',
            alpha=1.0,
            zorder=self.zorder_edges
        )
        self.plotter.axes.add_collection(collection)
        self._edgecollection = collection

    def draw_halfedges(self,
                       halfedges: Optional[List[Tuple[int, int]]] = None,
                       color: Union[str, Color, List[Color], Dict[int, Color]] = (0.7, 0.7, 0.7),
                       distance: float = 0.05,
                       width: float = 0.01,
                       shrink: float = 0.8,
                       ) -> None:
        """Draw a selection of halfedges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A list of halfedges to draw.
            The default is None, in which case all halfedges are drawn.
        color : rgb-tuple or dict[tuple[int, int], rgb-tuple], optional
            The color specification for the halfedges.

        Returns
        -------
        None

        """
        self.clear_halfedges()
        self._halfedgecollection = []

        if color:
            self.halfedge_color = color

        if halfedges:
            self.halfedges = halfedges

        for u, v in self.halfedges:
            face = self.mesh.halfedge_face(u, v)

            if face is None:
                normal = self.mesh.face_normal(self.mesh.halfedge_face(v, u))
            else:
                normal = self.mesh.face_normal(face)

            a, b = self.mesh.edge_coordinates(u, v)
            line = Line(* offset_line((a, b), distance, normal))
            frame = Frame(line.midpoint, [1, 0, 0], [0, 1, 0])
            scale = Scale.from_factors([shrink, shrink, shrink], frame=frame)
            line.transform(scale)

            artist = self.plotter.axes.arrow(
                line.start[0], line.start[1],
                line.vector[0], line.vector[1],
                width=width,
                head_width=10 * width,
                head_length=10 * width,
                length_includes_head=True,
                shape='right',
                color=self.halfedge_color.get((u, v), self.default_halfedgecolor),
                zorder=10000
            )
            self._halfedgecollection.append(artist)

    def draw_faces(self,
                   faces: Optional[List[int]] = None,
                   color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None
                   ) -> None:
        """Draw a selection of faces.

        Parameters
        ----------
        faces : list[int], optional
            A list of face identifiers.
            The default is None, in which case all faces are drawn.
        color : rgb-tuple or dict[int, rgb-tuple], optional
            The color specification for the faces.

        Returns
        -------
        None

        """
        self.clear_faces()
        if faces:
            self.faces = faces
        if color:
            self.face_color = color

        polygons = []
        facecolors = []
        edgecolors = []
        linewidths = []
        for face in self.faces:
            data = [self.vertex_xyz[vertex][:2] for vertex in self.mesh.face_vertices(face)]
            polygons.append(PolygonPatch(data))
            facecolors.append(self.face_color.get(face, self.default_facecolor))
            edgecolors.append((0, 0, 0))
            linewidths.append(0.1)

        collection = PatchCollection(
            polygons,
            facecolors=facecolors,
            edgecolors=edgecolors,
            lw=linewidths,
            alpha=1.0,
            linestyle='solid',
            zorder=self.zorder_faces
        )
        self.plotter.axes.add_collection(collection)
        self._facecollection = collection

    def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> None:
        """Draw a selection of vertex labels.

        Parameters
        ----------
        text : dict[int, str], optional
            A vertex-label map.

        Returns
        -------
        None

        """
        if self._vertexlabelcollection:
            for artist in self._vertexlabelcollection:
                artist.remove()

        if text:
            self.vertex_text = text

        labels = []
        for vertex in self.vertices:
            bgcolor = self.vertex_color.get(vertex, self.default_vertexcolor)
            color = (0, 0, 0) if is_color_light(bgcolor) else (1, 1, 1)

            text = self.vertex_text.get(vertex, None)
            if text is None:
                continue

            x, y = self.vertex_xyz[vertex][:2]
            artist = self.plotter.axes.text(
                x, y,
                f'{text}',
                fontsize=self.plotter.fontsize,
                family='monospace',
                ha='center', va='center',
                zorder=10000,
                color=color
            )
            labels.append(artist)

        self._vertexlabelcollection = labels

    def draw_edgelabels(self, text: Optional[Dict[int, str]] = None) -> None:
        """Draw a selection of edge labels.

        Parameters
        ----------
        text : dict[tuple[int, int], str]
            An edge-label map.

        Returns
        -------
        None

        """
        if self._edgelabelcollection:
            for artist in self._edgelabelcollection:
                artist.remove()

        if text:
            self.edge_text = text

        labels = []
        for edge in self.edges:
            u, v = edge
            text = self.edge_text.get(edge, self.edge_text.get((v, u), None))
            if text is None:
                continue

            x0, y0 = self.vertex_xyz[edge[0]][:2]
            x1, y1 = self.vertex_xyz[edge[1]][:2]
            x = 0.5 * (x0 + x1)
            y = 0.5 * (y0 + y1)

            artist = self.plotter.axes.text(
                x, y, f'{text}',
                fontsize=self.plotter.fontsize,
                family='monospace',
                ha='center', va='center',
                zorder=10000,
                color=(0, 0, 0),
                bbox=dict(boxstyle='round, pad=0.3', facecolor=(1, 1, 1), edgecolor=None, linewidth=0)
            )
            labels.append(artist)

        self._edgelabelcollection = labels

    def draw_facelabels(self, text: Optional[Dict[int, str]] = None) -> None:
        """Draw a selection of face labels.

        Parameters
        ----------
        text : dict[int, str]
            A face-label map.

        Returns
        -------
        None

        """
        if self._facelabelcollection:
            for artist in self._facelabelcollection:
                artist.remove()

        if text:
            self.face_text = text

        labels = []
        for face in self.faces:
            text = self.face_text.get(face, None)
            if text is None:
                continue

            x, y, _ = centroid_points_xy([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)])

            artist = self.plotter.axes.text(
                x, y, f'{text}',
                fontsize=self.plotter.fontsize,
                family='monospace',
                ha='center', va='center',
                zorder=10000,
                color=(0, 0, 0),
                bbox=dict(boxstyle='circle, pad=0.5', facecolor=(1, 1, 1), edgecolor=(0.5, 0.5, 0.5), linestyle=':')
            )
            labels.append(artist)

        self._facelabelcollection = labels

    def redraw(self) -> None:
        """Redraw the mesh using the current geometry.

        Returns
        -------
        None

        """
        pass

    def update_vertexcolors(self, colors):
        """Update the colors of the vertices.

        Parameters
        ----------
        colors : dict[int, tuple[float, float, float]]
            Mapping between vertex identifiers and colors.
            Missing vertices get the default color: :attr:`MeshArtist.default_vertexcolor`.

        Returns
        -------
        None

        """
        facecolors = []
        for vertex in self.vertices:
            if vertex in colors:
                color = colors[vertex]
            else:
                color = self.vertex_color.get(vertex, self.default_vertexcolor)
            facecolors.append(color)
        self._vertexcollection.set_facecolors(facecolors)

    def update_edgecolors(self, colors):
        """Update the colors of the edges.

        Parameters
        ----------
        colors : dict[tuple[int, int], tuple[float, float, float]]
            Mapping between edge identifiers and colors.
            Missing edge get the default color: :attr:`MeshArtist.default_edgecolor`.

        Returns
        -------
        None

        """
        edgecolors = []
        for edge in self.edges:
            if edge in colors:
                color = colors[edge]
            else:
                color = self.edge_color.get(edge, self.default_edgecolor)
            edgecolors.append(color)
        self._edgecollection.set_colors(edgecolors)

    def update_edgewidths(self, widths):
        """Update the widths of the edges.

        Parameters
        ----------
        widths : dict[tuple[int, int], float]
            Mapping between edge identifiers and linewidths.
            Missing edges get the default edge linewidth: :attr:`MeshArtist.default_edgewidth`.

        Returns
        -------
        None

        """
        edgewidths = []
        for edge in self.edges:
            if edge in widths:
                w = widths[edge]
            else:
                w = self.edge_width.get(edge, self.default_edgewidth)
            edgewidths.append(w)
        self._edgecollection.set_linewidths(edgewidths)


# src/compas_plotters/artists/networkartist.py

from typing import Dict
from typing import Tuple
from typing import List
from typing import Union
from typing import Optional
from typing_extensions import Literal

from matplotlib.collections import LineCollection, PatchCollection
from matplotlib.patches import Circle

from compas.datastructures import Network
from compas.artists import NetworkArtist
from compas.utilities.colors import is_color_light
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class NetworkArtist(PlotterArtist, NetworkArtist):
    """Artist for COMPAS network data structures.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A COMPAS network.
    nodes : list[int], optional
        Selection of node identifiers.
        Default is None, in which case all nodes are drawn.
    edges : list[tuple[int, int]], optional
        Selection of edge identifiers.
        The default is None, in which case all edges are drawn.
    nodecolor : tuple[float, float, float] or dict[int, tuple[float, float, float]], optional
        Color specification for the nodes.
    edgecolor : tuple[float, float, float] or dict[tuple[int, int], tuple[float, float, float]], optional
        Color specification for the edges.
    show_nodes : bool, optional
        If True, draw the nodes of the network.
    show_edges : bool, optional
        If True, draw the edges of the network.
    nodesize : int, optional
        The size of the nodes.
    sizepolicy : {'relative', 'absolute'}, optional
        The policy for sizing the nodes.
        If ``'relative'``, the value of `nodesize` is scaled by the number of nodes.
        If ``'absolute'``, the value of `nodesize` is scaled by the resolution of the plotter (:attr:NetworkArtist.plotter.dpi).

    Attributes
    ----------
    node_size : dict[int, float]
        Mapping between node identifiers and node sizes.
    zorder_edges : int, read-only
        The stacking order of the edges relative to the base stacking order of the network.
    zorder_nodes : int, read-only
        The stacking order of the nodes relative to the base stacking order of the network.

    """

    def __init__(self,
                 network: Network,
                 nodes: Optional[List[int]] = None,
                 edges: Optional[List[int]] = None,
                 nodecolor: Color = (1.0, 1.0, 1.0),
                 edgecolor: Color = (0.0, 0.0, 0.0),
                 edgewidth: float = 1.0,
                 show_nodes: bool = True,
                 show_edges: bool = True,
                 nodesize: int = 5,
                 sizepolicy: Literal['relative', 'absolute'] = 'relative',
                 zorder: int = 1000,
                 **kwargs):

        super().__init__(network=network, **kwargs)

        self.sizepolicy = sizepolicy
        self.nodes = nodes
        self.edges = edges
        self.node_color = nodecolor
        self.node_size = nodesize
        self.edge_color = edgecolor
        self.edge_width = edgewidth
        self.show_nodes = show_nodes
        self.show_edges = show_edges
        self.zorder = zorder

    @property
    def zorder_edges(self):
        return self.zorder + 10

    @property
    def zorder_nodes(self):
        return self.zorder + 20

    @property
    def item(self):
        """Network: Alias for ``~NetworkArtist.network``"""
        return self.network

    @item.setter
    def item(self, item: Network):
        self.network = item

    @property
    def data(self) -> List[List[float]]:
        return self.network.nodes_attributes('xy')

    @property
    def node_size(self):
        if not self._node_size:
            factor = self.plotter.dpi if self.sizepolicy == 'absolute' else self.network.number_of_nodes()
            size = self.default_nodesize / factor
            self._node_size = {node: size for node in self.network.nodes()}
        return self._node_size

    @node_size.setter
    def node_size(self, nodesize):
        factor = self.plotter.dpi if self.sizepolicy == 'absolute' else self.network.number_of_nodes()
        if isinstance(nodesize, dict):
            self.node_size.update({node: size / factor for node, size in nodesize.items()})
        elif isinstance(nodesize, (int, float)):
            self._node_size = {node: nodesize / factor for node in self.network.nodes()}

    # ==============================================================================
    # clear and draw
    # ==============================================================================

    def clear_nodes(self):
        """Clear the current nodes from the canvas.

        Returns
        -------
        None

        """
        if self._nodecollection:
            self.plotter.axes.remove_collection(self._nodecollection)
        self._nodecollection = None

    def clear_edges(self):
        """Clear the current edges from the canvas.

        Returns
        -------
        None

        """
        if self._edgecollection:
            self.plotter.axes.remove_collection(self._edgecollection)
        self._edgecollection = None

    def draw(self,
             nodes: Optional[List[int]] = None,
             edges: Optional[Tuple[int, int]] = None,
             nodecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None,
             edgecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> None:
        """Draw the network.

        Parameters
        ----------
        nodes : list[int], optional
            A list of node identifiers.
            Default is None, in which case all nodes are drawn.
        edges : list[tuple[int, int]], optional
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        nodecolor : tuple[float, float, float] or dict[int, tuple[float, float, float]], optional
            The color specification for the nodes.
        edgecolor : tuple[float, float, float] or dict[tuple[int, int], tuple[float, float, float]], optional
            The color specification for the edges.

        Returns
        -------
        None

        """
        self.clear()
        if self.show_nodes:
            self.draw_nodes(nodes=nodes, color=nodecolor)
        if self.show_edges:
            self.draw_edges(edges=edges, color=edgecolor)

    def draw_nodes(self,
                   nodes: Optional[List[int]] = None,
                   color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> None:
        """Draw a selection of nodes.

        Parameters
        ----------
        nodes : list[int], optional
            A list of node identifiers.
            Default is None, in which case all nodes are drawn.
        color : tuple[float, float, float] or dict[int, tuple[float, float, float]], optional
            The color specification for the nodes.

        Returns
        -------
        None

        """
        self.clear_nodes()
        if nodes:
            self.nodes = nodes
        if color:
            self.node_color = color

        circles = []
        for node in self.nodes:
            x, y = self.node_xyz[node][:2]
            circle = Circle(
                [x, y],
                radius=self.node_size.get(node, self.default_nodesize),
                facecolor=self.node_color.get(node, self.default_nodecolor),
                edgecolor=(0, 0, 0),
                lw=0.3,
            )
            circles.append(circle)

        collection = PatchCollection(
            circles,
            match_original=True,
            zorder=self.zorder_nodes,
            alpha=1.0
        )
        self.plotter.axes.add_collection(collection)
        self._nodecollection = collection

    def draw_edges(self,
                   edges: Optional[Tuple[int, int]] = None,
                   color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> None:
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A list of edge keys (as uv pairs) identifying which edges to draw.
            The default is None, in which case all edges are drawn.
        color : tuple[float, float, float] or dict[tuple[int, int], tuple[float, float, float]], optional
            The color specification for the edges.

        Returns
        -------
        None

        """
        self.clear_edges()
        if edges:
            self.edges = edges
        if color:
            self.edge_color = color

        lines = []
        colors = []
        widths = []
        for edge in self.edges:
            lines.append([self.node_xyz[edge[0]][:2], self.node_xyz[edge[1]][:2]])
            colors.append(self.edge_color.get(edge, self.default_edgecolor))
            widths.append(self.edge_width.get(edge, self.default_edgewidth))

        collection = LineCollection(
            lines,
            linewidths=widths,
            colors=colors,
            linestyle='solid',
            alpha=1.0,
            zorder=self.zorder_edges
        )
        self.plotter.axes.add_collection(collection)
        self._edgecollection = collection

    def draw_nodelabels(self, text: Optional[Dict[int, str]] = None) -> None:
        """Draw a selection of node labels.

        Parameters
        ----------
        text : dict[int, str], optional
            A node-label map.

        Returns
        -------
        None

        """
        if self._nodelabelcollection:
            for artist in self._nodelabelcollection:
                artist.remove()

        if text:
            self.node_text = text

        labels = []
        for node in self.nodes:
            bgcolor = self.node_color.get(node, self.default_nodecolor)
            color = (0, 0, 0) if is_color_light(bgcolor) else (1, 1, 1)

            text = self.node_text.get(node, None)
            print(text)
            if text is None:
                continue

            x, y = self.node_xyz[node][:2]
            artist = self.plotter.axes.text(
                x, y,
                f'{text}',
                fontsize=self.plotter.fontsize,
                family='monospace',
                ha='center', va='center',
                zorder=10000,
                color=color
            )
            labels.append(artist)

        self._nodelabelcollection = labels

    def draw_edgelabels(self, text: Optional[Dict[int, str]] = None) -> None:
        """Draw a selection of edge labels.

        Parameters
        ----------
        text : dict[tuple[int, int], str]
            An edge-label map.

        Returns
        -------
        None

        """
        if self._edgelabelcollection:
            for artist in self._edgelabelcollection:
                artist.remove()

        if text:
            self.edge_text = text

        labels = []
        for edge in self.edges:
            u, v = edge
            text = self.edge_text.get(edge, self.edge_text.get((v, u), None))
            if text is None:
                continue

            x0, y0 = self.node_xyz[edge[0]][:2]
            x1, y1 = self.node_xyz[edge[1]][:2]
            x = 0.5 * (x0 + x1)
            y = 0.5 * (y0 + y1)

            artist = self.plotter.axes.text(
                x, y, f'{text}',
                fontsize=self.plotter.fontsize,
                family='monospace',
                ha='center', va='center',
                zorder=10000,
                color=(0, 0, 0),
                bbox=dict(boxstyle='round, pad=0.3', facecolor=(1, 1, 1), edgecolor=None, linewidth=0)
            )
            labels.append(artist)

        self._edgelabelcollection = labels


# src/compas_plotters/artists/pointartist.py

from typing import Tuple
from typing import List
from typing import Any

from matplotlib.patches import Circle
from matplotlib.transforms import ScaledTranslation
from compas.geometry import Point

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class PointArtist(PlotterArtist, PrimitiveArtist):
    """Artist for COMPAS points.

    Parameters
    ----------
    point : :class:`compas.geometry.Point`
        A COMPAS point.
    size : int, optional
        The size of the point.
    facecolor : Color, optional
        Color of the interior of the point representing the point.
    edgecolor : Color, optional
        Color of the boundary of the point representing the point.
    zorder : int, optional
        Stacking order above the XY plane of the plotter canvas.
    **kwargs : dict, optional
        Additional keyword arguments. See :class:`PlotterArtist` or :class:`PrimitiveArtist`.

    Attributes
    ----------
    point : :class:`compas.geometry.Point`
        The point associated with the artist.
    size : float
        Size of the point, relative to the resolution of the plotter.
        ``size = self._size / self.plotter.dpi``.

    """

    def __init__(self,
                 point: Point,
                 size: int = 5,
                 facecolor: Color = (1.0, 1.0, 1.0),
                 edgecolor: Color = (0, 0, 0),
                 zorder: int = 9000,
                 **kwargs: Any):

        super().__init__(primitive=point, **kwargs)

        self._mpl_circle = None
        self._size = None
        self.size = size
        self.facecolor = facecolor
        self.edgecolor = edgecolor
        self.zorder = zorder

    @property
    def point(self) -> Point:
        return self.primitive

    @point.setter
    def point(self, point: Point):
        self.primitive = point

    @property
    def _T(self):
        F = self.plotter.figure.dpi_scale_trans
        S = ScaledTranslation(self.point[0], self.point[1], self.plotter.axes.transData)
        T = F + S
        return T

    @property
    def size(self) -> float:
        return self._size / self.plotter.dpi

    @size.setter
    def size(self, size: int):
        self._size = size

    @property
    def data(self) -> List[List[float]]:
        return [self.point[:2]]

    def draw(self) -> None:
        """Draw the circle.

        Returns
        -------
        None

        """
        circle = Circle(
            [0, 0],
            radius=self.size,
            facecolor=self.facecolor,
            edgecolor=self.edgecolor,
            transform=self._T,
            zorder=self.zorder
        )
        self._mpl_circle = self.plotter.axes.add_artist(circle)
        self.update_data()

    def redraw(self) -> None:
        """Update the point using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        self._mpl_circle.set_radius(self.size)
        self._mpl_circle.set_edgecolor(self.edgecolor)
        self._mpl_circle.set_facecolor(self.facecolor)
        self._mpl_circle.set_transform(self._T)
        self.update_data()


# src/compas_plotters/artists/polygonartist.py

from typing import Tuple
from typing import List
from typing import Any
from typing_extensions import Literal

from matplotlib.patches import Polygon as PolygonPatch
from compas.geometry import Polygon

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class PolygonArtist(PlotterArtist, PrimitiveArtist):
    """Artist for COMPAS polygons.

    Parameters
    ----------
    polygon : :class:`compas.geometry.Polygon`
        A COMPAS polygon.
    linewidth : float, optional
        Width of the polygon edge lines.
    linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional
        Style of the line.
    facecolor : tuple[float, float, float], optional
        Color of the interior face of the polygon.
    edgecolor : tuple[float, float, float], optional
        Color of the boundary of the polygon.
    zorder : int, optional
        Stacking order of the polygon on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    polygon : :class:`compas.geometry.Polygon`
        The polygon associated with the artist.

    """

    def __init__(self,
                 polygon: Polygon,
                 linewidth: float = 1.0,
                 linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid',
                 facecolor: Color = (1.0, 1.0, 1.0),
                 edgecolor: Color = (0, 0, 0),
                 fill: bool = True,
                 alpha: float = 1.0,
                 zorder: int = 1000,
                 **kwargs: Any):

        super().__init__(primitive=polygon, **kwargs)

        self._mpl_polygon = None
        self.linewidth = linewidth
        self.linestyle = linestyle
        self.facecolor = facecolor
        self.edgecolor = edgecolor
        self.fill = fill
        self.alpha = alpha
        self.zorder = zorder

    @property
    def polygon(self):
        return self.primitive

    @polygon.setter
    def polygon(self, polygon):
        self.primitive = polygon

    @property
    def data(self) -> List[List[float]]:
        return [point[:2] for point in self.polygon.points]

    def draw(self) -> None:
        """Draw the polygon.

        Returns
        -------
        None

        """
        polygon = PolygonPatch(self.data,
                               linewidth=self.linewidth,
                               linestyle=self.linestyle,
                               facecolor=self.facecolor,
                               edgecolor=self.edgecolor,
                               zorder=self.zorder,
                               alpha=self.alpha,
                               fill=self.fill)
        self._mpl_polygon = self.plotter.axes.add_patch(polygon)

    def redraw(self) -> None:
        """Update the polygon using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        self._mpl_polygon.set_xy(self.data)
        self._mpl_polygon.set_facecolor(self.facecolor)
        self._mpl_polygon.set_edgecolor(self.edgecolor)
        self._mpl_polygon.set_linewidth(self.linewidth)


# src/compas_plotters/artists/polylineartist.py

from typing import Tuple
from typing import List
from typing import Any
from typing_extensions import Literal

from matplotlib.lines import Line2D
from compas.geometry import Polyline

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class PolylineArtist(PlotterArtist, PrimitiveArtist):
    """Artist for COMPAS polylines.

    Parameters
    ----------
    polyline : :class:`compas.geometry.Polyline`
        A COMPAS polyline.
    linewidth : float, optional
        Width of the polyline edge lines.
    linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional
        Style of the line.
    facecolor : tuple[float, float, float], optional
        Color of the interior face of the polyline.
    edgecolor : tuple[float, float, float], optional
        Color of the boundary of the polyline.
    zorder : int, optional
        Stacking order of the polyline on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    polyline : :class:`compas.geometry.Polyline`
        The line associated with the artist.

    """

    def __init__(self,
                 polyline: Polyline,
                 draw_points: bool = True,
                 linewidth: float = 1.0,
                 linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid',
                 color: Color = (0, 0, 0),
                 zorder: int = 1000,
                 **kwargs: Any):

        super().__init__(primitive=polyline, **kwargs)

        self._mpl_line = None
        self._point_artists = []
        self.draw_points = draw_points
        self.linewidth = linewidth
        self.linestyle = linestyle
        self.color = color
        self.zorder = zorder

    @property
    def polyline(self):
        return self.primitive

    @polyline.setter
    def polyline(self, polyline):
        self.primitive = polyline

    @property
    def data(self) -> List[List[float]]:
        return [point[:2] for point in self.polyline.points]

    def draw(self) -> None:
        """Draw the polyline.

        Returns
        -------
        None

        """
        x, y, _ = zip(* self.polyline.points)
        line2d = Line2D(x, y,
                        linewidth=self.linewidth,
                        linestyle=self.linestyle,
                        color=self.color,
                        zorder=self.zorder)
        self._mpl_line = self.plotter.axes.add_line(line2d)
        if self.draw_points:
            for point in self.polyline:
                self._point_artists.append(self.plotter.add(point))

    def redraw(self) -> None:
        """Update the polyline using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        x, y, _ = zip(* self.polyline.points)
        self._mpl_line.set_xdata(x)
        self._mpl_line.set_ydata(y)
        self._mpl_line.set_color(self.color)
        self._mpl_line.set_linewidth(self.width)


# src/compas_plotters/artists/segmentartist.py

from typing import Tuple
from typing import List
from typing import Any
from typing_extensions import Literal

from matplotlib.lines import Line2D
from compas.geometry import Line

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class SegmentArtist(PlotterArtist, PrimitiveArtist):
    """Artist for drawing COMPAS lines as segments.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`
        A COMPAS line.
    draw_points : bool, optional
        If True, draw the start and end point of the line.
    linewidth : float, optional
        Width of the line.
    linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional
        Style of the line.
    color : tuple[float, float, float], optional
        Color of the line.
    zorder : int, optional
        Stacking order of the line on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    line : :class:`compas.geometry.Line`
        The line associated with the artist.

    """

    def __init__(self,
                 line: Line,
                 draw_points: bool = False,
                 linewidth: float = 2.0,
                 linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid',
                 color: Color = (0.0, 0.0, 0.0),
                 zorder: int = 2000,
                 **kwargs: Any):

        super().__init__(primitive=line, **kwargs)

        self._mpl_line = None
        self._start_artist = None
        self._end_artist = None
        self.draw_points = draw_points
        self.linestyle = linestyle
        self.linewidth = linewidth
        self.color = color
        self.zorder = zorder

    @property
    def line(self):
        return self.primitive

    @line.setter
    def line(self, line):
        self.primitive = line

    @property
    def data(self) -> List[List[float]]:
        return [self.line.start[:2], self.line.end[:2]]

    def draw(self) -> None:
        """Draw the line associated with the artist.

        Returns
        -------
        None

        """
        line2d = Line2D([self.line.start[0], self.line.end[0]], [self.line.start[1], self.line.end[1]],
                        linewidth=self.linewidth,
                        linestyle=self.linestyle,
                        color=self.color,
                        zorder=self.zorder)
        self._mpl_line = self.plotter.axes.add_line(line2d)
        if self.draw_points:
            self._start_artist = self.plotter.add(self.line.start, edgecolor=self.color)
            self._end_artist = self.plotter.add(self.line.end, edgecolor=self.color)

    def redraw(self) -> None:
        """Update the line using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        self._mpl_line.set_xdata([self.line.start[0], self.line.end[0]])
        self._mpl_line.set_ydata([self.line.start[1], self.line.end[1]])
        self._mpl_line.set_color(self.color)
        self._mpl_line.set_linewidth(self.width)
        if self.draw_points:
            self._start_artist.redraw()
            self._end_artist.redraw()


# src/compas_plotters/artists/vectorartist.py

from typing import Tuple
from typing import List
from typing import Any
from typing import Optional

from matplotlib.patches import FancyArrowPatch
from matplotlib.patches import ArrowStyle
from compas.geometry import Point, Vector

from compas.artists import PrimitiveArtist
from .artist import PlotterArtist

Color = Tuple[float, float, float]


class VectorArtist(PlotterArtist, PrimitiveArtist):
    """Artist for COMPAS vectors.

    Parameters
    ----------
    vector : :class:`compas.geometry.Vector`
        A COMPAS vector.
    point : :class:`compas.geometry.Point`, optional
        A COMPAS point as base point for the vector.
        Default is the origin of the world coordinate system.
    draw_point : bool, optional
        If True, draw the point of application of the vector.
    color : tuple[float, float, float], optional
        Color of the vector.
    zorder : int, optional
        Stacking order of the vector on the canvas.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`compas_plotters.artists.PlotterArtist` and :class:`compas.artists.PrimitiveArtist` for more info.

    Attributes
    ----------
    vector : :class:`compas.geometry.Vector`
        The vector associated with the artist.

    """

    def __init__(self,
                 vector: Vector,
                 point: Optional[Point] = None,
                 draw_point: bool = False,
                 color: Color = (0, 0, 0),
                 zorder: int = 3000,
                 **kwargs: Any):

        super().__init__(primitive=vector, **kwargs)

        self._mpl_vector = None
        self._point_artist = None
        self.draw_point = draw_point
        self.point = point or Point(0.0, 0.0, 0.0)
        self.color = color
        self.zorder = zorder

    @property
    def vector(self):
        return self.primitive

    @vector.setter
    def vector(self, vector):
        self.primitive = vector

    @property
    def data(self) -> List[List[float]]:
        return [self.point[:2], (self.point + self.vector)[:2]]

    def draw(self) -> None:
        """Draw the vector.

        Returns
        -------
        None

        """
        style = ArrowStyle("Simple, head_length=0.1, head_width=0.1, tail_width=0.02")
        arrow = FancyArrowPatch(self.point[:2], (self.point + self.vector)[:2],
                                arrowstyle=style,
                                edgecolor=self.color,
                                facecolor=self.color,
                                zorder=self.zorder,
                                mutation_scale=100)
        if self.draw_point:
            self._point_artist = self.plotter.add(self.point, edgecolor=self.color)
        self._mpl_vector = self.plotter.axes.add_patch(arrow)

    def redraw(self):
        """Update the vector using the current geometry and visualization settings.

        Returns
        -------
        None

        """
        self._mpl_vector.set_positions(self.point[:2], (self.point + self.vector)[:2])
        if self.draw_point:
            self._point_artist.redraw()


# src/compas_plotters/core/__init__.py

from .utilities import (
    get_axes_dimension,
    assert_axes_dimension,
    width_to_dict,
    size_to_sizedict
)
from .helpers import (
    Axes2D,
    Axes3D,
    Bounds,
    Box,
    Cloud2D,
    Cloud3D,
    Hull
)
from .drawing import (
    create_axes_xy,
    create_axes_3d,
    draw_points_xy,
    draw_xpoints_xy,
    draw_points_3d,
    draw_lines_xy,
    draw_xlines_xy,
    draw_lines_3d,
    draw_xarrows_xy,
    draw_xlabels_xy,
    draw_xpolygons_xy,
    draw_xpolylines_xy
)

__all__ = [
    'get_axes_dimension',
    'assert_axes_dimension',
    'width_to_dict',
    'size_to_sizedict',
    'Axes2D',
    'Axes3D',
    'Bounds',
    'Box',
    'Cloud2D',
    'Cloud3D',
    'Hull',
    'create_axes_xy',
    'create_axes_3d',
    'draw_points_xy',
    'draw_xpoints_xy',
    'draw_points_3d',
    'draw_lines_xy',
    'draw_xlines_xy',
    'draw_lines_3d',
    'draw_xarrows_xy',
    'draw_xlabels_xy',
    'draw_xpolygons_xy',
    'draw_xpolylines_xy'
]


# src/compas_plotters/core/drawing.py

from numpy import asarray

import matplotlib.pyplot as plt

from matplotlib.patches import Circle
from matplotlib.patches import Polygon

from matplotlib.collections import LineCollection
from matplotlib.collections import PatchCollection
from matplotlib.collections import PolyCollection

from mpl_toolkits.mplot3d.art3d import Line3DCollection

from compas.geometry import centroid_points_xy
from compas.geometry import midpoint_line_xy
from compas.utilities import color_to_rgb


__all__ = [
    'create_axes_xy',
    'create_axes_3d',
    'draw_points_xy',
    'draw_xpoints_xy',
    'draw_points_3d',
    'draw_lines_xy',
    'draw_xlines_xy',
    'draw_lines_3d',
    'draw_xarrows_xy',
    'draw_xlabels_xy',
    'draw_xpolygons_xy',
    'draw_xpolylines_xy',
]


ZORDER_POLYGONS = 1000
ZORDER_LINES = 2000
ZORDER_POINTS = 3000
ZORDER_LABELS = 4000


# ==============================================================================
# axes
# ==============================================================================


def create_axes_xy(figsize=(8.0, 6.0),
                   dpi=100,
                   xlabel=None,
                   ylabel=None,
                   fontname='Times New Roman',
                   fontsize=10,
                   grid=False,
                   xlim=None,
                   ylim=None,
                   ticklength=20,
                   tickfontsize=10,
                   xscale='linear',
                   yscale='linear',
                   bgcolor='#ffffff'):
    """Initializes plot axes object for matplotlib plotting.

    Parameters
    ----------
    figsize : 2-tuple of float, optional
        Size of the figure.
        Default is ``(8.0, 6.0)``
    dpi : int, optional
        Resolution of the plot.
        Default is ``100``.
    xlabel : str, optional
        Label for the x-axis.
        Default is None.
    ylabel : str, optional
        Label for the y-axis.
        Default is None.
    fontname : str, optional
        Fontname of the main labels and text.
        Default is ``'Times New Roman'``.
    fontsize : int, optional
        Fontsize of the main labels and text.
        Default is ``10``.
    grid : bool, optional
        Display grid.
        Default is False.
    xlim : 2-tuple, optional
        Limits of the X-axis.
        Default is None.
    ylim : 2-tuple, optional
        Limits of the Y-axis.
        Default is None.
    ticklength : float, optional
        Length of the ticks.
        Default is ``20``.
    tickfontsize : int, optional
        Fontsize of the ticks.
        Default is ``10``.
    xscale : {'linear', 'log'}
        Scale of the X axis.
    yscale : {'linear', 'log'}
        Scale of the Y axis.
    bgcolor : str or list, optional
        Background color as hex string or rgb tuple.
        Default is white.

    Returns
    -------
    object
        Matplotlib axes.

    """
    # mpl.rcParams['figure.figsize'] = figsize
    # mpl.rcParams['figure.dpi'] = dpi
    # mpl.rcParams['savefig.dpi'] = dpi
    fig = plt.figure(facecolor=bgcolor, figsize=figsize, dpi=dpi)
    axes = fig.add_subplot(111, aspect='equal')
    axes.grid(b=grid)
    axes.set_frame_on(False)
    if xlabel:
        axes.set_xlabel(xlabel, fontname=fontname, fontsize=fontsize)
    if ylabel:
        axes.set_ylabel(ylabel, fontname=fontname, fontsize=fontsize)
    if xlim:
        axes.set_xlim(xlim[0], xlim[1])
    if ylim:
        axes.set_ylim(ylim[0], ylim[1])
    # plt.minorticks_on()
    # plt.tick_params(which='major', length=ticklength, labelsize=tickfontsize)
    # plt.tick_params(which='minor', length=ticklength * 0.33)
    axes.set_xscale(xscale)
    axes.set_yscale(yscale)
    axes.set_xticks([])
    axes.set_yticks([])
    # axes.set_xmargin(0.05)
    # axes.set_ymargin(0.05)
    # axes.autoscale()
    return axes


def create_axes_3d(size=(10, 7),
                   xlabel='$x$',
                   ylabel='$y$',
                   zlabel='$z$',
                   fontname='Times New Roman',
                   fontsize=20,
                   grid=True,
                   limits=None,
                   ticklength=20,
                   tickfontsize=15,
                   angle=(30, 45)):
    """Initializes plot axes object for matplotlib plotting.

    Parameters
    ----------
    size : 2-tuple of float, optinoal
        Size of the figure.
        Default is ``(10.0, 7.0)``.
    xlabel : str, optional
        Label for the x-axis.
        Default is ``'$x$'``.
    ylabel : str, optional
        Label for the y-axis.
        Default is ``'$y$'``.
    zlabel : str, optional
        Label for the z-axis.
        Default is ``'$z$'``.
    fontname : str, optional
        Fontname of the main labels and text.
        Default is ``'Times New Roman'``.
    fontsize : int, optional
        Fontsize of the main labels and text.
        Default is ``10``.
    grid : bool, optional
        Display grid.
        Default is False.
    limits : dict, optional
        Axis limits and tick spacing.
        Default is None.
    ticklength : float, optional
        Length of the ticks.
        Default is ``20``.
    tickfontsize : int, optional
        Fontsize of the ticks.
        Default is ``15``.
    angle : 2-tuple of float
        Elevation and azimuth angles for 3D plots.
        Default is ``'30.0, 45.0'``.

    Returns
    -------
    object
        Matplotlib axes.

    """
    fig = plt.figure(facecolor='white', figsize=size)
    axes = fig.add_subplot(111, projection='3d', aspect='equal')
    # axes.w_xaxis.set_pane_color((1, 1, 1, 1))
    # axes.w_yaxis.set_pane_color((1, 1, 1, 1))
    # axes.w_zaxis.set_pane_color((1, 1, 1, 1))
    axes.grid(b=grid)
    axes.set_xlabel(xlabel, fontname=fontname, fontsize=fontsize)
    axes.set_ylabel(ylabel, fontname=fontname, fontsize=fontsize)
    axes.set_zlabel(zlabel, fontname=fontname, fontsize=fontsize)
    # axes.view_init(elev=angle[0], azim=angle[1])
    axes.set_xticks([])
    axes.set_yticks([])
    axes.set_zticks([])
    axes.set_xmargin(0.05)
    axes.set_ymargin(0.05)
    axes.autoscale()
    return axes


# ==============================================================================
# points
# ==============================================================================


def draw_points_xy(points,
                   axes,
                   facecolor='#ffffff',
                   edgecolor='#000000',
                   linewidth=0.5,
                   radius=1.0):
    """Creates an XY point collection and adds it to the axis.

    Parameters
    ----------
    points : list
        XY(Z) coordinates of the points.
    axes : object
        Matplotlib axes.
    facecolor : str or list, optional
        Color of the point face.
        Default is white.
    edgecolor : str or list, optional
        Color of the point edge.
        Default is black.
    linewidth : float or list, optional
        Width of the point edge.
        Default is ``0.5``.
    radius : float or list, optional
        The radius of the points.
        Default is ``1.0``.

    Returns
    -------
    object
        A collection of points.

    """
    p = len(points)
    # preprocess patch parameters
    if isinstance(facecolor, str):
        facecolor = [facecolor] * p
    if isinstance(edgecolor, str):
        edgecolor = [edgecolor] * p
    if isinstance(linewidth, (int, float)):
        linewidth = float(linewidth)
        linewidth = [linewidth] * p
    if isinstance(radius, (int, float)):
        radius = float(radius)
        radius = [radius] * p
    # --------------------------------------------------------------------------
    circles = []
    for i in range(p):
        point = points[i]
        circle = Circle(point[0:2], radius=radius[i])
        circles.append(circle)
    coll = PatchCollection(
        circles,
        facecolors=facecolor,
        edgecolors=edgecolor,
        linewidhts=linewidth,
        alpha=1.0,
        zorder=ZORDER_POINTS
    )
    axes.add_collection(coll)
    return coll


def draw_xpoints_xy(points, axes):
    """Creates an XY point collection and adds it to the axis.

    Parameters
    ----------
    points : list of dict
        List of dictionaries containing the point properties.
        Each point is represented by a circle with a given radius.
        The following properties of the circle can be specified in the point dict.

        * pos (list): XY(Z) coordinates
        * radius (float, optional): the radius of the circle. Default is 0.1.
        * text (str, optional): the text of the label. Default is None.
        * facecolor (rgb or hex color, optional): The color of the face of the circle. Default is white.
        * edgecolor (rgb or hex color, optional): The color of the edge of the cicrle. Default is black.
        * edgewidth (float, optional): The width of the edge of the circle. Default is 1.0.
        * textcolor (rgb or hex color, optional): Color of the text label. Default is black.
        * fontsize (int, optional): Font size of the text label. Default is 12.

    axes : object
        Matplotlib axes.

    Returns
    -------
    object
        The matplotlib point collection object.

    """
    circles = []
    facecolors = []
    edgecolors = []
    linewidths = []
    for point in points:
        pos = point['pos']
        radius = point['radius']
        text = point.get('text')
        fcolor = point.get('facecolor') or '#ffffff'
        ecolor = point.get('edgecolor') or '#000000'
        lwidth = point.get('edgewidth') or 1.0
        textcolor = point.get('textcolor') or '#000000'
        fontsize = point.get('fontsize') or 12
        circles.append(Circle(pos[0:2], radius=radius))
        facecolors.append(color_to_rgb(fcolor, normalize=True))
        edgecolors.append(color_to_rgb(ecolor, normalize=True))
        linewidths.append(lwidth)
        if text is not None:
            axes.text(
                pos[0] - 0.01,
                pos[1] - 0.01,
                text,
                fontsize=fontsize,
                zorder=ZORDER_LABELS,
                ha='center',
                va='center',
                color=textcolor
            )
    coll = PatchCollection(
        circles,
        linewidths=linewidths,
        facecolors=facecolors,
        edgecolors=edgecolors,
        alpha=1.0,
        zorder=ZORDER_POINTS
    )
    axes.add_collection(coll)
    return coll


def draw_points_3d(points,
                   axes,
                   facecolor='#ffffff',
                   edgecolor='#000000'):
    """Creates a 3D point collection and adds it to the axis.

    Parameters
    ----------
    points : list
        XYZ coordinates of the points.
    axes : object
        Matplotlib axes.
    facecolor : str or list, optional
        Color of the face of the points.
        Default is white.
    edgecolor : str or list, optional
        Color of the edge of the points.
        Default is black.

    Returns
    -------
    object
        The matplotlib point collection object.

    """
    p = len(points)
    points = asarray(points)
    if isinstance(facecolor, str):
        facecolor = [facecolor] * p
    if isinstance(edgecolor, str):
        edgecolor = [edgecolor] * p
    x = points[:, 0]
    y = points[:, 1]
    z = points[:, 2]
    coll, _ = axes.plot(x, y, z, 'o', color=(1.0, 1.0, 1.0))
    return coll


# ==============================================================================
# lines
# ==============================================================================


def draw_lines_xy(lines,
                  axes,
                  linewidth=1.0,
                  linestyle='-',
                  color='#000000',
                  alpha=1.0):
    """Creates an XY line collection and adds it to the axis.

    Parameters
    ----------
    lines : list
        List of ((X1, Y1), (X2, X2)) lines.
    axes : object
        Matplotlib axes.
    linewidth : float or list of float, optional
        Width of the lines.
        Default is ``1.0``.
    linestyle : str or list of str, optional
        Matplotlib line style strings.
        Default is ``'-'``.
    color : str or list of str, optional
        Color of the lines.
        Default is black.
    alpha : float or list of float, optional
        Opacity of the lines.
        Default is ``1.0``.

    Returns
    -------
    object
        The matplotlib point collection object.

    """
    n = len(lines)
    if isinstance(linewidth, (int, float)):
        linewidth = float(linewidth)
        linewidth = [linewidth] * n
    if isinstance(color, str):
        color = [color] * n
    # --------------------------------------------------------------------------
    coll = LineCollection(
        [(start[0:2], end[0:2]) for start, end in lines],
        linewidths=linewidth,
        colors=color,
        linestyle=linestyle,
        alpha=alpha,
        zorder=ZORDER_LINES
    )
    axes.add_collection(coll)
    return coll


def draw_xlines_xy(lines, axes, alpha=1.0, linestyle='solid'):
    """Creates an XY line collection and adds it to the axis.

    Parameters
    ----------
    lines : list
        List of dictionaries containing the line properties.
        The following properties of a line can be specified in the dict.

        * start (list): XY(Z) coordinates of the start point.
        * end (list): XY(Z) coordinatesof the end point.
        * width (float, optional): The width of the line. Default is ``1.0``.
        * color (rgb tuple or hex string, optional): The color of the line. Default is black.
        * text (str, optional): The text of the label. Default is None.
        * textcolor (rgb tuple or hex string, optional): Color of the label text. Default is black.
        * fontsize (int, optional): The size of the font of the label text. Default is ```12``.

    axes : object
        Matplotlib axes.
    alpha : float, optional
        Opacity of the lines.
        Default is ``1.0``.
    linestyle : str, optional
        Matplotlib line style strings.
        Default is ``'solid'``.

    Returns
    -------
    object
        The matplotlib line collection object.

    """
    fromto = []
    widths = []
    colors = []
    for line in lines:
        sp = line['start']
        ep = line['end']
        width = line.get('width', 1.0)
        color = line.get('color', '#000000')
        text = line.get('text', None)
        textcolor = line.get('textcolor') or '#000000'
        fontsize = line.get('fontsize') or 6
        fromto.append((sp[0:2], ep[0:2]))
        widths.append(width)
        colors.append(color_to_rgb(color, normalize=True))
        if text:
            x, y, z = midpoint_line_xy((sp, ep))
            t = axes.text(x,
                          y,
                          text,
                          fontsize=fontsize,
                          zorder=ZORDER_LABELS,
                          ha='center',
                          va='center',
                          color=color_to_rgb(textcolor, normalize=True))
            t.set_bbox({'color': '#ffffff', 'alpha': 1.0})
    coll = LineCollection(
        fromto,
        linewidths=widths,
        colors=colors,
        linestyle=linestyle,
        alpha=alpha,
        zorder=ZORDER_LINES
    )
    axes.add_collection(coll)
    return coll


def draw_lines_3d(lines,
                  axes,
                  linewidth=1.0,
                  linestyle='solid',
                  color='#000000'):
    """Creates an 3D line collection and adds it to the axis.

    Parameters
    ----------
    lines : list
        Pairs of XYZ coordinates defining start and end points of the lines.
    axes : object
        Matplotlib axes.
    linewidth : float or list of float, optional
        Width for the lines.
        Default is ``1.0``.
    linestyle : str, optional
        Matplotlib line style strings.
        Default is ``'solid'``.
    color : str or list of str, optional
        Color of the lines.
        Default is black.

    Returns
    -------
    object
        The matplotlib line collection object.

    """
    n = len(lines)
    if isinstance(linewidth, (int, float)):
        linewidth = float(linewidth)
        linewidth = [linewidth] * n
    if isinstance(color, str):
        color = [color] * n

    coll = Line3DCollection(
        lines,
        linewidths=linewidth,
        colors=color,
        linestyle=linestyle,
        zorder=ZORDER_LINES
    )
    axes.add_collection(coll)
    return coll


# ==============================================================================
# polylines
# ==============================================================================


def draw_xpolylines_xy(polylines, axes):
    paths = []

    widths = []
    colors = []

    for polyline in polylines:
        points = polyline['points']
        width = polyline.get('width', 1.0)
        color = polyline.get('color', '#000000')
        text = polyline.get('text', None)
        textcolor = polyline.get('textcolor') or '#000000'
        fontsize = polyline.get('fontsize') or 6

        path = [point[0:2] for point in points]
        paths.append(path)

        widths.append(width)
        colors.append(color_to_rgb(color, normalize=True))

        if text:
            p = len(points)

            if p % 2 == 0:
                a = points[p // 2]
                b = points[p // 2 + 1]
                x, y, z = midpoint_line_xy((a, b))
            else:
                x, y = points[p // 2 + 1]

            t = axes.text(x,
                          y,
                          text,
                          fontsize=fontsize,
                          zorder=ZORDER_LABELS,
                          ha='center',
                          va='center',
                          color=color_to_rgb(textcolor, normalize=True))

            t.set_bbox({'color': '#ffffff', 'alpha': 1.0, 'edgecolor': '#ffffff'})

    coll = PolyCollection(
        paths,
        closed=False,
        linewidths=widths,
        edgecolors=colors,
        facecolors='none',
        zorder=ZORDER_LINES
    )

    axes.add_collection(coll)

    return coll


# ==============================================================================
# arrows
# ==============================================================================


def draw_xarrows_xy(lines, axes):
    """Creates an XY arrow collection and adds it to the axis.

    Parameters
    ----------
    lines : list of dict
        List of dictionaries containing the arrow line properties.
        The following properties of an arrow can be specified in the dict.

        * start (list): XY(Z) coordinates of the starting point.
        * end (list): XY(Z) coordinates of the end point.
        * text (str, optional): The text of the label. Default is None.
        * textcolor (rgb tuple or hex string, optional): Color of the label text. Default is black.
        * fontsize (int, optional): The size of the font of the label text. Default is ```6``.
        * color (rgb tuple or hex string, optional): Color of the arrow. Default is black.
        * width (float): Width of the arrow. Default is ``1.0``.

    axes : object
        Matplotlib axes.

    """
    arrowprops = {
        'arrowstyle': '-|>,head_length=0.6,head_width=0.2',
        'connectionstyle': 'arc3,rad=0.0',
        'linewidth': 1.0,
        'color': '#000000',
        'shrinkB': 0.0,
        'shrinkA': 0.0,
    }
    xys = []
    for line in lines:
        sp = line['start'][:2]
        ep = line['end'][:2]
        text = line.get('text', None)
        textcolor = line.get('textcolor') or '#000000'
        fontsize = line.get('fontsize') or 6
        arrowprops['color'] = color_to_rgb(line.get('color', '#000000'), normalize=True)
        arrowprops['linewidth'] = line.get('width', 1.0)
        axes.annotate(
            '',
            xy=ep,
            xytext=sp,
            arrowprops=arrowprops,
            zorder=ZORDER_LINES,
        )
        xys.append(sp)
        xys.append(ep)
        if text:
            x, y, z = midpoint_line_xy((sp, ep))
            t = axes.text(x,
                          y,
                          text,
                          fontsize=fontsize,
                          zorder=ZORDER_LABELS,
                          ha='center',
                          va='center',
                          color=color_to_rgb(textcolor, normalize=True))
            t.set_bbox({'color': '#ffffff', 'alpha': 1.0, 'edgecolor': '#ffffff'})
    axes.update_datalim(xys)


# ==============================================================================
# labels
# ==============================================================================


def draw_xlabels_xy(labels, axes):
    """Creates a label collection and adds it to the axis.

    Parameters
    ----------
    labels : list of dict
        List of dictionaries containing the label properties.
    axes : object
        Matplotlib axes.

    """
    for label in labels:
        x, y = label['pos']
        text = label['text']
        fontsize = label['fontsize']
        color = label.get('color') or '#ffffff'
        textcolor = label.get('textcolor') or '#000000'
        bbox = dict(color=color_to_rgb(color, normalize=True),
                    edgecolor=color_to_rgb(color, normalize=True),
                    alpha=1.0,
                    pad=0.0)
        t = axes.text(
            x,
            y,
            text,
            fontsize=fontsize,
            zorder=ZORDER_LABELS,
            ha='center',
            va='center',
            color=color_to_rgb(textcolor, normalize=True)
        )
        t.set_bbox(bbox)


# ==============================================================================
# faces
# ==============================================================================


def draw_xpolygons_xy(polygons, axes):
    """Creates a polygon collection and adds it to the axis.

    Parameters
    ----------
    polygons : list of dict
        List of dictionaries containing the polygon properties.
        The following properties can be specified in the dict.

        * points (list): XY(Z) coordinates of the polygon vertices.
        * text (str, optional): The text of the label. Default is None.
        * textcolor (rgb tuple or hex string, optional): Color of the label text. Default is black.
        * fontsize (int, optional): The size of the font of the label text. Default is ```12``.
        * facecolor (rgb tuple or hex string, optional): Color of the polygon face. Default is white.
        * edgecolor (rgb tuple or hex string, optional): Color of the edge of the polygon. Default is black.
        * edgewidth (float): Width of the polygon edge. Default is ``1.0``.

    axes : object
        Matplotlib axes.

    Returns
    -------
    object
        The matplotlib polygon collection object.

    """
    facecolors = []
    edgecolors = []
    linewidths = []
    patches = []

    for attr in polygons:
        points = attr['points']
        text = attr.get('text')
        textcolor = color_to_rgb(attr.get('textcolor', '#000000'), normalize=True)

        facecolors.append(color_to_rgb(attr.get('facecolor', '#ffffff'), normalize=True))
        edgecolors.append(color_to_rgb(attr.get('edgecolor', '#000000'), normalize=True))
        linewidths.append(attr.get('edgewidth', 1.0))

        patches.append(Polygon([point[0:2] for point in points]))

        if text:
            c = centroid_points_xy(points)
            axes.text(
                c[0],
                c[1],
                text,
                fontsize=attr.get('fontsize', 10.0),
                zorder=ZORDER_LABELS,
                ha='center',
                va='center',
                color=textcolor
            )

    coll = PatchCollection(
        patches,
        facecolors=facecolors,
        edgecolors=edgecolors,
        lw=linewidths,
        zorder=ZORDER_POLYGONS
    )

    axes.add_collection(coll)

    return coll


# src/compas_plotters/core/helpers.py

from numpy import asarray
from numpy import argmax
from numpy import argmin
from numpy import zeros

from mpl_toolkits.mplot3d.art3d import Poly3DCollection

from compas_plotters.core.utilities import assert_axes_dimension


__all__ = [
    'Axes2D', 'Axes3D', 'Bounds', 'Box', 'Cloud2D', 'Cloud3D', 'Hull',
]


class Axes2D(object):
    """Definition of a 2D Axes object.

    Parameters
    ----------
    origin : tuple or list
        X and Y coordinates for the origin.
    vectors : list
        The X and Y axes.

    Attributes
    ----------
    origin : tuple or list
        X and Y coordinates for the origin.
    vectors : list
        The X and Y axes.

    """

    def __init__(self, origin, vectors):
        """Initializes the Axes2D object"""
        self.origin = asarray(origin)
        self.vectors = asarray(vectors)

    def plot(self, axes):
        """Plots the axes object

        Parameters
        ----------
        axes : object
            The matplotlib axes object.

        """
        assert_axes_dimension(axes, 2)
        o = self.origin
        xy = self.vectors
        axes.plot(
            [o[0, 0], o[0, 0] + xy[0, 0]],
            [o[0, 1], o[0, 1] + xy[0, 1]],
            'r-'
        )
        axes.plot(
            [o[0, 0], o[0, 0] + xy[1, 0]],
            [o[0, 1], o[0, 1] + xy[1, 1]],
            'g-'
        )


class Axes3D(object):
    """Definition of a 3D Axes object.

    Parameters
    ----------
    origin : tuple or list
        X, Y and Z coordinates for the origin.
    vectors : list
        The X, Y and Z axes.

    Attributes
    ----------
    origin : tuple or list
        X, Y and Z coordinates for the origin.
    vectors : list
        The X, Y and Z axes.

    """

    def __init__(self, origin, vectors, colors=None):
        """Initializes the Axes3D object"""
        self.origin = asarray(origin)
        self.vectors = asarray(vectors)
        if not colors:
            colors = ('r', 'g', 'b')
        self.colors = colors

    def plot(self, axes):
        """Plots the axes object

        Parameters
        ----------
        axes : object
            The matplotlib axes object.
        """
        assert_axes_dimension(axes, 3)
        o = self.origin
        xyz = self.vectors
        axes.plot(
            [o[0, 0], o[0, 0] + xyz[0, 0]],
            [o[0, 1], o[0, 1] + xyz[0, 1]],
            [o[0, 2], o[0, 2] + xyz[0, 2]],
            '{0}-'.format(self.colors[0]),
            linewidth=3
        )
        axes.plot(
            [o[0, 0], o[0, 0] + xyz[1, 0]],
            [o[0, 1], o[0, 1] + xyz[1, 1]],
            [o[0, 2], o[0, 2] + xyz[1, 2]],
            '{0}-'.format(self.colors[1]),
            linewidth=3
        )
        axes.plot(
            [o[0, 0], o[0, 0] + xyz[2, 0]],
            [o[0, 1], o[0, 1] + xyz[2, 1]],
            [o[0, 2], o[0, 2] + xyz[2, 2]],
            '{0}-'.format(self.colors[2]),
            linewidth=3
        )


class Bounds(object):
    """"""

    def __init__(self, points):
        self.points = asarray(points)

    def plot(self, axes):
        assert_axes_dimension(axes, 3)
        xmin, ymin, zmin = argmin(self.points, axis=0)
        xmax, ymax, zmax = argmax(self.points, axis=0)
        xspan = self.points[xmax, 0] - self.points[xmin, 0]
        yspan = self.points[ymax, 1] - self.points[ymin, 1]
        zspan = self.points[zmax, 2] - self.points[zmin, 2]
        span = max(xspan, yspan, zspan)
        axes.plot([self.points[xmin, 0]], [self.points[ymin, 1]], [self.points[zmin, 2]], 'w')
        axes.plot([self.points[xmin, 0] + span], [self.points[ymin, 1] + span], [self.points[zmin, 2] + span], 'w')


class Box(object):
    """"""

    def __init__(self, corners):
        self.corners = corners
        self.faces = [[0, 1, 2, 3], [4, 7, 6, 5], [1, 5, 6, 2], [0, 4, 5, 1], [0, 3, 7, 4], [2, 6, 7, 3]]

    def plot(self, axes):
        assert_axes_dimension(axes, 3)
        rec = [[self.corners[index] for index in face] for face in self.faces]
        rec_coll = Poly3DCollection(rec)
        rec_coll.set_facecolors([(1.0, 0.0, 0.0) for face in self.faces])
        rec_coll.set_alpha(0.2)
        axes.add_collection3d(rec_coll)


class Cloud2D(object):
    """"""

    def __init__(self, cloud):
        cloud = asarray(cloud)
        cols = min(2, cloud.shape[1])
        self.cloud = zeros((cloud.shape[0], 2))
        self.cloud[:, :cols] = cloud[:, :cols]

    def plot(self, axes):
        x = self.cloud[:, 0]
        y = self.cloud[:, 1]
        axes.plot(x, y, 'o', color=(1.0, 1.0, 1.0))


class Cloud3D(object):
    """"""

    def __init__(self, cloud):
        cloud = asarray(cloud)
        cols = min(3, cloud.shape[1])
        self.cloud = zeros((cloud.shape[0], 3))
        self.cloud[:, :cols] = cloud[:, :cols]

    def plot(self, axes):
        x = self.cloud[:, 0]
        y = self.cloud[:, 1]
        z = self.cloud[:, 2]
        axes.plot(x, y, z, 'o', color=(0.7, 0.7, 0.7))


class Hull(object):
    """"""

    def __init__(self, hull):
        self.vertices = hull.points
        self.faces = hull.simplices

    def plot(self, axes):
        tri = [[self.vertices[index] for index in face] for face in self.faces]
        tri_coll = Poly3DCollection(tri)
        tri_coll.set_facecolors([(0.0, 1.0, 0.0) for face in self.faces])
        axes.add_collection3d(tri_coll)


# src/compas_plotters/core/utilities.py

__all__ = [
    'get_axes_dimension',
    'assert_axes_dimension',
    'width_to_dict',
    'size_to_sizedict',
]


def get_axes_dimension(axes):
    """Returns the number of dimensions of a matplotlib axes object.

        Parameters
        ----------
        axes : object
            The matplotlib axes object.

        Returns
        -------
        int
            The number of dimensions of a matplotlib axes object.
        """
    if hasattr(axes, 'get_zlim'):
        return 3
    else:
        return 2


def assert_axes_dimension(axes, dim):
    """Asserts if the number of dimensions of a matplotlib axes equals a given dim.

        Parameters
        ----------
        axes : object
            The matplotlib axes object to assert.
        dim :  int
            The numbers of dimensions to assert with.

        Returns
        -------
        bool
            True if the axes object has dim dimensions.
        """
    assert get_axes_dimension(axes) == dim, 'The provided axes are not {0}D.'.format(dim)


def width_to_dict(width, dictkeys, defval=None):
    width = width or defval
    if isinstance(width, (int, float)):
        return dict((key, width) for key in dictkeys)
    if isinstance(width, dict):
        for k, w in width.items():
            if isinstance(w, (int, float)):
                width[k] = w
        return dict((key, width.get(key, defval)) for key in dictkeys)
    raise Exception('This is not a valid width format: {0}'.format(type(width)))


def size_to_sizedict(size, dictkeys, defval=None):
    size = size or defval
    if isinstance(size, (int, float)):
        return dict((key, size) for key in dictkeys)
    if isinstance(size, dict):
        for k, s in size.items():
            if isinstance(s, (int, float)):
                size[k] = s
        return dict((key, size.get(key, defval)) for key in dictkeys)
    raise Exception('This is not a valid size format: {0}'.format(type(size)))


def synchronize_scale_axes(axes):
    pass


# docs/conf.py

# flake8: noqa
# -*- coding: utf-8 -*-

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = "1.0"

import sys
import os
import inspect
import importlib
import m2r2

import sphinx_compas_theme
from sphinx.ext.napoleon.docstring import NumpyDocstring

sys.path.append(os.path.join(os.path.dirname(__file__), '_ext'))

# patches

current_m2r2_setup = m2r2.setup


def patched_m2r2_setup(app):
    try:
        return current_m2r2_setup(app)
    except (AttributeError):
        app.add_source_suffix(".md", "markdown")
        app.add_source_parser(m2r2.M2RParser)
    return dict(
        version=m2r2.__version__, parallel_read_safe=True, parallel_write_safe=True,
    )


m2r2.setup = patched_m2r2_setup

# -- General configuration ------------------------------------------------

project = "COMPAS"
copyright = "Block Research Group - ETH Zurich"
author = "Tom Van Mele"

release = "1.13.3"
version = ".".join(release.split(".")[0:2])

master_doc = "index"
source_suffix = {
    ".rst": "restructuredtext",
    ".md": "markdown",
}
templates_path = sphinx_compas_theme.get_autosummary_templates_path()
exclude_patterns = ["_build", "**.ipynb_checkpoints", "_notebooks", "**/__temp"]

pygments_style = "sphinx"
show_authors = True
add_module_names = True
language = None


# -- Extension configuration ------------------------------------------------

extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.autosummary",
    "sphinx.ext.doctest",
    "sphinx.ext.intersphinx",
    "sphinx.ext.mathjax",
    "sphinx.ext.napoleon",
    "sphinx.ext.linkcode",
    "sphinx.ext.extlinks",
    "sphinx.ext.githubpages",
    "sphinx.ext.coverage",
    "sphinx.ext.inheritance_diagram",
    "sphinx.ext.graphviz",
    "matplotlib.sphinxext.plot_directive",
    "m2r2",
    "nbsphinx",
    "sphinx.ext.autodoc.typehints",
    "tabs"
]

# autodoc options

autodoc_type_aliases = {}

# this does not work properly yet
autodoc_typehints = "none"
autodoc_typehints_format = "short"
autodoc_typehints_description_target = "documented"

autodoc_mock_imports = [
    "System",
    "clr",
    "Eto",
    "Rhino",
    "Grasshopper",
    "scriptcontext",
    "rhinoscriptsyntax",
    "bpy",
    "bmesh",
    "mathutils"
]

autodoc_default_options = {
    "undoc-members": True,
    "show-inheritance": True,
}

autodoc_member_order = "groupwise"

autoclass_content = "class"


def skip(app, what, name, obj, would_skip, options):
    if name.startswith('_'):
        return True
    return would_skip


def setup(app):
    app.connect("autodoc-skip-member", skip)


# autosummary options

autosummary_generate = True
autosummary_mock_imports = [
    "System",
    "clr",
    "Eto",
    "Rhino",
    "Grasshopper",
    "scriptcontext",
    "rhinoscriptsyntax",
    "bpy",
    "bmesh",
    "mathutils"
]

# graph options

inheritance_graph_attrs = dict(rankdir="LR", resolution=150)
inheritance_node_attrs = dict(fontsize=8)

# napoleon options

napoleon_google_docstring = False
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = True
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_param = False
napoleon_use_rtype = False


# first, we define new methods for any new sections and add them to the class
def parse_keys_section(self, section):
    return self._format_fields("Keys", self._consume_fields())


NumpyDocstring._parse_keys_section = parse_keys_section


def parse_attributes_section(self, section):
    return self._format_fields("Attributes", self._consume_fields())


NumpyDocstring._parse_attributes_section = parse_attributes_section


def parse_class_attributes_section(self, section):
    return self._format_fields("Class Attributes", self._consume_fields())


NumpyDocstring._parse_class_attributes_section = parse_class_attributes_section


# we now patch the parse method to guarantee that the the above methods are
# assigned to the _section dict
def patched_parse(self):
    self._sections["keys"] = self._parse_keys_section
    self._sections["attributes"] = self._parse_attributes_section
    self._sections["class attributes"] = self._parse_class_attributes_section
    self._unpatched_parse()


NumpyDocstring._unpatched_parse = NumpyDocstring._parse
NumpyDocstring._parse = patched_parse


# plot options

plot_include_source = False
plot_html_show_source_link = False
plot_html_show_formats = False
plot_formats = ['png']
# plot_pre_code
# plot_basedir
# plot_rcparams
# plot_apply_rcparams
# plot_working_directory

plot_template = """
{{ only_html }}

   {% for img in images %}
   {% set has_class = false %}

   .. figure:: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }}
      :class: figure-img img-fluid

      {{ caption }}

   {% endfor %}
"""

# intersphinx options

intersphinx_mapping = {
    "python": ("https://docs.python.org/", None),
    "compas": ("https://compas.dev/compas/latest/", None),
}

# linkcode


def linkcode_resolve(domain, info):
    if domain != 'py':
        return None
    if not info['module']:
        return None
    if not info['fullname']:
        return None

    package = info['module'].split('.')[0]
    if not package.startswith('compas'):
        return None

    module = importlib.import_module(info['module'])
    parts = info['fullname'].split('.')

    if len(parts) == 1:
        obj = getattr(module, info['fullname'])
        mod = inspect.getmodule(obj)
        if not mod:
            return None
        filename = mod.__name__.replace('.', '/')
        lineno = inspect.getsourcelines(obj)[1]
    elif len(parts) == 2:
        obj_name, attr_name = parts
        obj = getattr(module, obj_name)
        attr = getattr(obj, attr_name)
        if inspect.isfunction(attr):
            mod = inspect.getmodule(attr)
            if not mod:
                return None
            filename = mod.__name__.replace('.', '/')
            lineno = inspect.getsourcelines(attr)[1]
        else:
            return None
    else:
        return None

    return f"https://github.com/compas-dev/compas/blob/main/src/{filename}.py#L{lineno}"


# extlinks


extlinks = {
    "rhino": ("https://developer.rhino3d.com/api/RhinoCommon/html/T_%s.htm", "%s"),
    "blender": ("https://docs.blender.org/api/2.93/%s.html", "%s")
}

# -- Options for HTML output ----------------------------------------------

html_theme = "compas"
html_theme_path = sphinx_compas_theme.get_html_theme_path()
html_theme_options = {
    "navbar_active": "compas",
    "package_version": release,
    "package_docs": "https://compas.dev/compas/",
    "package_old_versions_txt": "https://compas.dev/compas/doc_versions.txt"
}
html_context = {}
html_static_path = sphinx_compas_theme.get_html_static_path()
html_extra_path = []
html_last_updated_fmt = ""
html_copy_source = False
html_show_sourcelink = False
html_permalinks = False
html_permalinks_icon = ""
html_experimental_html5_writer = False
html_compact_lists = True


# tests/gen.py

from compas.geometry.transformations import transformations as src  # change this line to different modules


temp = ''
print(src.__all__)

for func in src.__all__:
    temp += 'from compas.geometry.transformations import {}\n'.format(func)

temp += '\n\n'

for func in src.__all__:
    temp += '''
def test_{}():
    pass

'''.format(func)

print(temp)


# tests/ipy_test_runner.py

from __future__ import print_function

import os

import pytest

HERE = os.path.dirname(__file__)

if __name__ == '__main__':
    # Fake Rhino modules
    pytest.load_fake_module('Rhino')
    pytest.load_fake_module('Rhino.Geometry', fake_types=['RTree', 'Sphere', 'Point3d'])

    pytest.run(HERE)


# src/compas_rhino/forms/etoforms/__init__.py

"""
********************************************************************************
etoforms
********************************************************************************

.. currentmodule:: compas_rhino.etoforms

.. note::

    In the future, Eto forms will be merged into the normal forms package.

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    ImageForm
    PropertyListForm
    SettingsForm
    TextForm

"""
from __future__ import absolute_import

from .image import *  # noqa: F401 F403
from .propertylist import *  # noqa: F401 F403
from .settings import *  # noqa: F401 F403
from .text import *  # noqa: F401 F403


__all__ = [name for name in dir() if not name.startswith('_')]


# src/compas_rhino/forms/etoforms/image.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

try:
    basestring
except NameError:
    basestring = str

from System.Net import WebClient
from System.IO import MemoryStream

import clr
clr.AddReference("Eto")
clr.AddReference("Rhino.UI")

import Rhino  # noqa: E402
import Rhino.UI  # noqa: E402

import Eto.Drawing  # noqa: E402
import Eto.Forms  # noqa: E402


__all__ = ['ImageForm']


def image_from_remote(source):
    """Construct an image from a remote source.

    Parameters
    ----------
    source : str
        The url of the remote source.

    Returns
    -------
    System.Drawing.Image
        Representation of an miage in memory.

    Examples
    --------
    .. code-block:: python

        image = image_from_remote('http://block.arch.ethz.ch/brg/images/cache/dsc02360_ni-2_cropped_1528706473_624x351.jpg')

    """
    w = WebClient()
    d = w.DownloadData(source)
    m = MemoryStream(d)
    return Eto.Drawing.Bitmap(m)


def image_from_local(source):
    """Construct an image from a local source.

    Parameters
    ----------
    source : str
        The path to the local source file.

    Returns
    -------
    System.Drawing.Image
        Representation of an miage in memory.

    Examples
    --------
    .. code-block:: python

        image = image_from_local('theblock.jpg')

    """
    return Eto.Drawing.Bitmap(source)


class ImageForm(Eto.Forms.Dialog[bool]):
    """Windows form for displaying images.

    Parameters
    ----------
    image : {str, Image}
        The image that should be displayed.
        This can be a url of a remote image file,
        or a local file path,
        or an instance of ``System.Drawing.Image``.
    title : str, optional
        Title of the form.
        Default is ``ImageForm``.
    width : int, optional
        Width of the form.
        Default is None.
    height : int, optional
        Height of the form.
        Default is None.

    Examples
    --------
    >>> from compas_rhino.forms import ImageForm
    >>> form = ImageForm('http://block.arch.ethz.ch/brg/images/cache/dsc02360_ni-2_cropped_1528706473_624x351.jpg')
    >>> form.show()

    """

    def __init__(self, image, title='Image', width=None, height=None):
        super(ImageForm, self).__init__(title, width, height)
        self._image = None
        self.image = image

        view = Eto.Forms.ImageView()
        view.Image = self.image
        self.Content = view

        # self.DefaultButton.Click += self.on_ok
        # self.AbortButton.Click += self.on_cancel

    @property
    def image(self):
        """Eto.Drawing.Image: An instance of ``Eto.Drawing.Image``.
        """
        return self._image

    @image.setter
    def image(self, image):
        if isinstance(image, basestring):
            if image.startswith('http'):
                self._image = image_from_remote(image)
            else:
                self._image = image_from_local(image)
        elif isinstance(image, Eto.Drawing.Image):
            self._image = image
        else:
            raise NotImplementedError

    def on_ok(self, sender, e):
        pass

    def on_cancel(self, sender, e):
        pass

    def show(self):
        return self.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)


# src/compas_rhino/forms/etoforms/propertylist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import clr
clr.AddReference("Eto")
clr.AddReference("Rhino.UI")

import Rhino  # noqa: E402
import Rhino.UI  # noqa: E402
import Eto.Drawing  # noqa: E402
import Eto.Forms  # noqa: E402


__all__ = ['PropertyListForm']


class PropertyListForm(Eto.Forms.Dialog[bool]):

    def __init__(self, names, values, title='Properties', width=400, height=600):
        self.names = names
        self.values = values

        self.table = table = Eto.Forms.GridView()
        table.ShowHeader = True
        table.DataStore = [[name, value] for name, value in zip(self.names, self.values)]

        c1 = Eto.Forms.GridColumn()
        c1.HeaderText = 'Name'
        c1.Editable = False
        c1.DataCell = Eto.Forms.TextBoxCell(0)
        table.Columns.Add(c1)

        c2 = Eto.Forms.GridColumn()
        c2.HeaderText = 'Value'
        c2.Editable = True
        c2.DataCell = Eto.Forms.TextBoxCell(1)
        table.Columns.Add(c2)

        tab_items = Eto.Forms.StackLayoutItem(table, True)
        layout = Eto.Forms.StackLayout()
        layout.Items.Add(tab_items)
        layout.HorizontalContentAlignment = Eto.Forms.HorizontalAlignment.Stretch
        sub_layout = Eto.Forms.DynamicLayout()
        sub_layout.AddRow(None, self.ok, self.cancel)
        layout.Items.Add(Eto.Forms.StackLayoutItem(sub_layout))

        self.Title = title
        self.Padding = Eto.Drawing.Padding(12)
        self.Resizable = True
        self.Content = layout
        self.ClientSize = Eto.Drawing.Size(width, height)

    @property
    def ok(self):
        self.DefaultButton = Eto.Forms.Button(Text='OK')
        self.DefaultButton.Click += self.on_ok
        return self.DefaultButton

    @property
    def cancel(self):
        self.AbortButton = Eto.Forms.Button(Text='Cancel')
        self.AbortButton.Click += self.on_cancel
        return self.AbortButton

    def on_ok(self, sender, event):
        try:
            for i, name in enumerate(self.names):
                value = self.table.DataStore[i][1]
                self.values[i] = value
        except Exception as e:
            print(e)
            self.Close(False)
        self.Close(True)

    def on_cancel(self, sender, event):
        self.Close(False)

    def show(self):
        return self.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)


# src/compas_rhino/forms/etoforms/settings.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from ast import literal_eval

import clr
clr.AddReference("Eto")
clr.AddReference("Rhino.UI")

import Rhino  # noqa: E402
import Rhino.UI  # noqa: E402
import Eto.Drawing  # noqa: E402
import Eto.Forms  # noqa: E402


__all__ = ['SettingsForm']


class SettingsForm(Eto.Forms.Dialog[bool]):

    def __init__(self, settings, title='Settings'):
        # super(SettingsForm, self).__init__()
        self._settings = None
        self._names = None
        self._values = None
        self.settings = settings

        self.table = table = Eto.Forms.GridView()
        table.ShowHeader = True
        table.DataStore = [[name, value] for name, value in zip(self.names, self.values)]
        table.Height = 300

        c1 = Eto.Forms.GridColumn()
        c1.HeaderText = 'Name'
        c1.Editable = False
        c1.DataCell = Eto.Forms.TextBoxCell(0)
        table.Columns.Add(c1)

        c2 = Eto.Forms.GridColumn()
        c2.HeaderText = 'Value'
        c2.Editable = True
        c2.DataCell = Eto.Forms.TextBoxCell(1)
        table.Columns.Add(c2)

        layout = Eto.Forms.DynamicLayout()
        layout.AddRow(table)
        layout.Add(None)
        layout.BeginVertical()
        layout.BeginHorizontal()
        layout.AddRow(None, self.ok, self.cancel)
        layout.EndHorizontal()
        layout.EndVertical()

        self.Title = title
        self.Padding = Eto.Drawing.Padding(12)
        self.Resizable = False
        self.Content = layout
        self.ClientSize = Eto.Drawing.Size(400, 600)

    @property
    def ok(self):
        self.DefaultButton = Eto.Forms.Button(Text='OK')
        self.DefaultButton.Click += self.on_ok
        return self.DefaultButton

    @property
    def cancel(self):
        self.AbortButton = Eto.Forms.Button(Text='Cancel')
        self.AbortButton.Click += self.on_cancel
        return self.AbortButton

    @property
    def settings(self):
        return self._settings

    @settings.setter
    def settings(self, settings):
        self._settings = settings.copy()
        self._names = names = sorted(settings.keys())
        self._values = [str(settings[name]) for name in names]

    @property
    def names(self):
        return self._names

    @property
    def values(self):
        return self._values

    def on_ok(self, sender, event):
        try:
            for i, name in enumerate(self.names):
                value = self.table.DataStore[i][1]
                try:
                    value = literal_eval(value)
                except (TypeError, ValueError, SyntaxError):
                    pass
                self._settings[name] = value
        except Exception as e:
            print(e)
            self.Close(False)
        self.Close(True)

    def on_cancel(self, sender, event):
        self.Close(False)

    def show(self):
        return self.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)


# src/compas_rhino/forms/etoforms/text.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import clr
clr.AddReference("Eto")
clr.AddReference("Rhino.UI")

import Rhino  # noqa: E402
import Rhino.UI  # noqa: E402
import Eto.Drawing  # noqa: E402
import Eto.Forms  # noqa: E402


__all__ = ['TextForm']


class TextForm(Eto.Forms.Dialog[bool]):

    def __init__(self, text, title='Message'):
        self.text = text
        self.textbox = textbox = Eto.Forms.TextArea()

        textbox.ReadOnly = True
        textbox.Append(text)

        layout = Eto.Forms.DynamicLayout()
        layout.AddRow(textbox)
        layout.Add(None)
        layout.BeginVertical()
        layout.BeginHorizontal()
        layout.AddRow(None, self.ok, self.cancel)
        layout.EndHorizontal()
        layout.EndVertical()

        self.Title = title
        self.Padding = Eto.Drawing.Padding(12)
        self.Resizable = False
        self.Content = layout
        self.ClientSize = Eto.Drawing.Size(400, 600)

    @property
    def ok(self):
        self.DefaultButton = Eto.Forms.Button(Text='OK')
        self.DefaultButton.Click += self.on_ok
        return self.DefaultButton

    @property
    def cancel(self):
        self.AbortButton = Eto.Forms.Button(Text='Cancel')
        self.AbortButton.Click += self.on_cancel
        return self.AbortButton

    def on_ok(self, sender, e):
        self.Close(True)

    def on_cancel(self, sender, e):
        self.Close(False)

    def show(self):
        return self.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)


# src/compas/__init__.py

"""
********************************************************************************
compas
********************************************************************************

.. currentmodule:: compas

.. toctree::
    :maxdepth: 1
    :titlesonly:

    compas.artists
    compas.colors
    compas.data
    compas.datastructures
    compas.files
    compas.geometry
    compas.numerical
    compas.plugins
    compas.robots
    compas.rpc
    compas.topology
    compas.utilities


"""
from __future__ import print_function

import os
import decimal

from distutils.version import LooseVersion

import compas._os
from compas._os import is_windows, is_linux, is_osx, is_mono, is_ironpython, is_rhino, is_blender, is_grasshopper  # noqa: F401
from compas.data import json_dump, json_dumps, json_load, json_loads


__author__ = 'Tom Van Mele and many others (see AUTHORS.md)'
__copyright__ = 'Copyright 2014-2019 - Block Research Group, ETH Zurich'
__license__ = 'MIT License'
__email__ = 'vanmelet@ethz.ch'

__version__ = '1.13.3'

version = LooseVersion(compas.__version__)
versionstring = version.vstring.split('-')[0]

HERE = os.path.dirname(__file__)
"""str: Path to the location of the compas package."""

HOME = compas._os.absjoin(HERE, '../..')
"""str: Path to the root of the local repo."""

DATA = compas._os.absjoin(HERE, '../../data')
"""str: Path to the data folder of the local repo."""

TEMP = compas._os.absjoin(HERE, '../../temp')
"""str: Path to the temp folder of the local repo."""

APPDATA = compas._os.user_data_dir('COMPAS', 'compas-dev', roaming=True)
"""str: Path to the COMPAS directory in APPDATA."""

APPTEMP = compas._os.absjoin(APPDATA, 'temp')
"""str: Path to a temp folder in the COMPAS directory in APPDATA."""

PRECISION = '3f'
"""str:
The precision used by COMPAS for generation of geometric keys,
for the comparison of point locations,
for the parsing of geometry files,
and for the generation of human-readable representations of geometry objects.

The string is in the format used by the Python string formating mini language
for formatting numbers.
Float formatting (``'<x>f'``) and integer formatting (``'d'``) specifiers are supported.
"""

PY3 = compas._os.PY3
"""bool: True if the current Python version is 3.x, False otherwise."""

WINDOWS = is_windows()
"""bool: True if the current operating system is Windows, False otherwise."""

LINUX = is_linux()
"""bool: True if the current operating system is Linux, False otherwise."""

OSX = is_osx()
"""bool: True if the current operating system is OSX, False otherwise."""

MONO = is_mono()
"""bool: True if the current operating system is Mono, False otherwise."""

IPY = is_ironpython()
"""bool: True if the current Python implementation is IronPython, False otherwise."""

RHINO = is_rhino()
"""bool: True if the current context is Rhino, False otherwise."""

BLENDER = is_blender()
"""bool: True if the current context is Blender, False otherwise."""


# Check if COMPAS is installed from git
# If that's the case, try to append the current head's hash to __version__
try:
    git_head_file = compas._os.absjoin(HOME, '.git', 'HEAD')

    if os.path.exists(git_head_file):
        # git head file contains one line that looks like this:
        # ref: refs/heads/main
        with open(git_head_file, 'r') as git_head:
            _, ref_path = git_head.read().strip().split(' ')
            ref_path = ref_path.split('/')

            git_head_refs_file = compas._os.absjoin(HOME, '.git', *ref_path)

        if os.path.exists(git_head_refs_file):
            with open(git_head_refs_file, 'r') as git_head_ref:
                git_commit = git_head_ref.read().strip()
                __version__ += '-' + git_commit[:8]
except Exception:
    pass


__all__ = [
    'WINDOWS', 'LINUX', 'OSX', 'MONO', 'IPY', 'RHINO', 'BLENDER',
    'is_windows', 'is_linux', 'is_osx', 'is_mono', 'is_ironpython', 'is_rhino', 'is_blender',
    'set_precision',
    'get',
    'json_dump', 'json_load', 'json_dumps', 'json_loads',
]


def set_precision(precision):
    """Set the precision used by geometric maps.

    Parameters
    ----------
    precision : float
        The precision as a floating point number.
        For example, ``0.0001``.

    Notes
    -----
    This function converts the floating point number to a string formatting
    specifier and assigns the specifier to ``compas.PRECISION``.

    Examples
    --------
    >>> compas.set_precision(0.001)
    >>> compas.PRECISION
    '3f'

    """
    global PRECISION
    precision = str(precision)
    d = decimal.Decimal(precision).as_tuple()
    if d.exponent < 0:
        e = - d.exponent
        PRECISION = "{}f".format(e)


# ==============================================================================
# data
# ==============================================================================

def get(filename):
    """Get the full path to one of the sample data files.

    Parameters
    ----------
    filename : str
        The name of the data file.
        The following are available.

        * boxes.obj
        * faces.obj
        * fink.obj
        * hypar.obj
        * lines.obj
        * saddle.obj

    Returns
    -------
    str
        The full path to the specified file.

    Notes
    -----
    The file name should be specified relative to the **COMPAS** sample data folder.
    This folder is only locally available if you installed **COMPAS** from source,
    or if you are working directly with the source.
    In all other cases, the function will get the corresponding files directly from
    the GitHub repo, at https://raw.githubusercontent.com/compas-dev/compas/main/data

    Examples
    --------
    The ``compas.get`` function is meant to be used in combination with the static
    constructors of the data structures.

    .. code-block:: python

        import compas
        from compas.datastructures import Mesh

        mesh = Mesh.from_obj(compas.get('faces.obj'))

    """
    filename = filename.strip('/')

    if filename.endswith('bunny.ply'):
        return get_bunny()

    localpath = compas._os.absjoin(DATA, filename)

    if os.path.exists(localpath):
        return localpath
    else:
        return "https://github.com/compas-dev/compas/raw/main/data/{}".format(filename)


def get_bunny(localstorage=None):
    """Get the *Stanford Bunny* directly from the Stanford repository.

    Parameters
    ----------
    localstorage : str, optional
        Path to a local storage folder for saving the downloaded data.
        Default is ``None``, in which case the data will be stored in a local
        user data directory. See https://pypi.org/project/appdirs/ for more info.

    Returns
    -------
    str
        Full path to the local file.

    Examples
    --------
    The *Stanford Bunny* is a `PLY` file.
    Therefore, the returned path should be used in combination with the ``PLY``
    file reader, or with the ``from_ply`` constructor function for meshes.

    .. code-block:: python

        import compas
        from compas.datastructures import Mesh

        mesh = Mesh.from_ply(compas.get_bunny())

    """
    import tarfile

    try:
        from urllib.request import urlretrieve
    except ImportError:
        from urllib import urlretrieve

    if not localstorage:
        localstorage = APPDATA

    if not os.path.exists(localstorage):
        os.makedirs(localstorage)

    if not os.path.isdir(localstorage):
        raise Exception('Local storage location does not exist: {}'.format(localstorage))

    if not os.access(localstorage, os.W_OK):
        raise Exception('Local storage location is not writable: {}'.format(localstorage))

    bunny = compas._os.absjoin(localstorage, 'bunny/reconstruction/bun_zipper.ply')
    destination = compas._os.absjoin(localstorage, 'bunny.tar.gz')

    if not os.path.exists(bunny):
        url = 'http://graphics.stanford.edu/pub/3Dscanrep/bunny.tar.gz'

        print('Getting the bunny from {} ...'.format(url))
        print('This will take a few seconds...')

        urlretrieve(url, destination)

        with tarfile.open(destination) as file:
            file.extractall(localstorage)

        os.remove(destination)

        print('Got it!\n')

    return bunny


# src/compas/__main__.py

# -*- coding: utf-8 -*-
from __future__ import print_function

import sys
import pkg_resources
import compas


if __name__ == '__main__':

    # c = 'DCDHDCACDHDCAEDEACDHDCAEDEACDHDCAEDCDEACDHDCADCACDEADHDCAEDADEACDHDADADADHDCACDCAEDEACDCACDHDCAEDEACDCAEDEACDCAEDBACDHDAEDEACDADADCAEDBADHDAGDEACDADEADCAEDEADHDBADEDCAEDEACDEDAGDHDADCAEDACDCADADADHDAGDADEACAEDADBADHDAGDCADEAEDEACDBADHDAGDCAEDADEACDBADHDBADADADADAGDHDAGDCADEDADBADHDBADADAGDHDEADEAEDEAEDADHDEADEDADEDADHDEACDADCAEDHDACDADCADHDEACDADCAEDHDEACDADCAEDHDEACDADCAEDHDEAFCDADCAEDHDEAEDHDEDH'  # noqa: E501
    # r = 'fGfB]DSD]BYBHEIEHCXBUCFBYBFCUBSBEBOEOBEBSBQBEPBGBPBEQBOBDBRIRBDBOBNEUGUENBLBECRBCBCBCBRCEBLBKBDBBBDBNBCBEBCBNBDBBBDBKBKDBFCDBIDIDIBDCFBDKBJDBKCCCDDKBCDCCCKBDJBIBDPCBBCBMBCBBCPDBIBIERBCBBBCGCBCDREIBIDBQDEBDCDBEDQBDIBIDBOBDIBCBIBCBOBDIBIDBNBCBKCKBCBNBDIBIBDMDMCMDMDBIBJDBHBFNCNGHBDJBJBDGkGDBJBKBDFBGB[BGBFEKBLBDHCPCPCHELBMBDBWCWBDBMBOEBUCUBEOBPBEBSCSBEBPBRBEBQCQBEBRBUBECMCMCECTBXBFBDGCGDGCWB[DXC[BbObB'  # noqa: E501
    # maps = ' !-X_`|\n' if compas.IPY or compas.WINDOWS else ' ▌▀█▄`▐\n'

    # for n, o in zip(r, c):
    #     print((ord(n) - 65) * maps[ord(o) - 65], end='')

    print()
    print('Yay! COMPAS is installed correctly!')
    print()
    print('COMPAS: {}'.format(compas.__version__))
    print('Python: {}'.format(str(sys.version)))

    working_set = pkg_resources.working_set
    packages = set([p.project_name for p in working_set]) - set(['COMPAS'])
    compas_pkgs = [p for p in packages if p.lower().startswith('compas')]

    if compas_pkgs:
        print('Extensions: {}'.format([p for p in compas_pkgs]))


# src/compas/_iotools.py

"""For the time being, these functions are only for internal use."""
from contextlib import contextmanager
import io

try:
    from urllib.request import urlopen
except ImportError:
    from urllib2 import urlopen


@contextmanager
def open_file(file_or_filename, mode='r'):
    """Context-manager to open a file, path or URL and return a corresponding file object.

    If the argument is a path-like object, it will open it,
    if the argument is an HTTP(s) URL, it will open it, and
    if the argument is already a file-like object, it will simply yield it.

    This context manager ensures that when using a path-like object as input,
    the file is closed once it it goes out of scope.

    Notes
    -----
    This context-manager will only close files that it opened itself. If an opened file
    is passed as the argument, it will not close it.

    Parameters
    ----------
    file_or_filename : path-like, file-like object or URL string
        A path-like, a file-like object or a URL pointing to a file.
    mode : str, optional
        Specifies the mode in which the file is opened. It defaults to ``'r'`` which means open for reading in text mode.

    Yields
    -------
    file-like object
        File object already opened.
    """
    file = file_or_filename
    close_source = False

    if not hasattr(file, 'read'):
        # If it's a URL, open and read its response into an in-memory stream
        if hasattr(file, 'startswith') and file.startswith('http'):
            # support all read modes (r, r+, rb, etc)
            if not mode.startswith('r'):
                raise ValueError('URLs can only be opened in read mode.')
            response = urlopen(file)
            file = io.BytesIO(response.read())
        else:
            file = open(file, mode=mode)
            close_source = True
    try:
        yield file
    finally:
        if close_source:
            file.close()


def iter_file(file, size=65536):
    """Iterate over a file returning chunks of data until EOF.

    Parameters
    ----------
    file : file-like object
        A file-like object.
    size : int, optional
        Size of each chuck of data to be read. Defaults to ``65536``.

    Yields
    -------
    bytes
        Byte array chunks read from the file.
    """
    while True:
        data = file.read(size)
        if not data:
            break
        yield data


# src/compas/_os.py

# -*- coding: utf-8 -*-
"""
These are internal functions of the framework.
Not intended to be used outside compas* packages.
"""
import os
import shutil
import sys
import tempfile
import subprocess

try:
    NotADirectoryError
except NameError:
    class NotADirectoryError(Exception):
        pass

PY3 = sys.version_info[0] == 3


__all__ = [
    'absjoin',
    'create_symlink',
    'create_symlinks',
    'remove_symlink',
    'remove_symlinks',
    'copy',
    'remove',
    'rename',
    'user_data_dir',
    'select_python',
    'prepare_environment',
    'is_admin',
    'is_windows',
    'is_linux',
    'is_osx',
    'is_mono',
    'is_ironpython',
    'is_rhino',
    'is_blender',
    'is_grasshopper'
]


def is_windows():
    """Check if the operating system is Windows.

    Returns
    -------
    bool
        True if the OS is Windows. False otherwise

    """
    if is_ironpython():
        return os.name == 'nt'
    return sys.platform == 'win32'


def is_linux():
    """Check if the operating system is Linux.

    Returns
    -------
    bool
        True if the OS is Linux. False otherwise

    """
    return sys.platform in ('linux', 'linux2')


def is_osx():
    return sys.platform == 'darwin'


def is_mono():
    """Check if the operating system is running on Mono.

    Returns
    -------
    bool
        True if the OS is running on Mono. False otherwise

    """
    return 'mono' in sys.version.lower()


def is_ironpython():
    """Check if the Python implementation is IronPython.

    Returns
    -------
    bool
        True if the implementation is IronPython. False otherwise

    """
    return 'ironpython' in sys.version.lower()


def is_rhino():
    try:
        import Rhino  # noqa : F401
    except ImportError:
        return False
    return True


def is_grasshopper():
    try:
        import Rhino
        import scriptcontext
    except ImportError:
        return False
    return not isinstance(scriptcontext.doc, Rhino.RhinoDoc)


def is_blender():
    try:
        import bpy  # noqa : F401
    except ImportError:
        return False
    return True


if is_windows():
    import ctypes
    import ctypes.wintypes

    class ShellExecuteInfo(ctypes.Structure):
        _fields_ = [
            ('cbSize',       ctypes.wintypes.DWORD),
            ('fMask',        ctypes.c_ulong),
            ('hwnd',         ctypes.wintypes.HWND),
            ('lpVerb',       ctypes.c_char_p),
            ('lpFile',       ctypes.c_char_p),
            ('lpParameters', ctypes.c_char_p),
            ('lpDirectory',  ctypes.c_char_p),
            ('nShow',        ctypes.c_int),
            ('hInstApp',     ctypes.wintypes.HINSTANCE),
            ('lpIDList',     ctypes.c_void_p),
            ('lpClass',      ctypes.c_char_p),
            ('hKeyClass',    ctypes.wintypes.HKEY),
            ('dwHotKey',     ctypes.wintypes.DWORD),
            ('hIcon',        ctypes.wintypes.HANDLE),
            ('hProcess',     ctypes.wintypes.HANDLE)]

        def __init__(self, **kw):
            super(ShellExecuteInfo, self).__init__()
            self.cbSize = ctypes.sizeof(self)
            for field_name, field_value in kw.items():
                setattr(self, field_name, field_value)

    SEE_MASK_NOCLOSEPROCESS = 0x00000040
    SEE_MASK_NO_CONSOLE = 0x00008000
    INFINITE = -1


# seems a bit weird that this is done here, since it is only relevant for Rhino/GH
# also, perhasps it makes more sense this is done at the top of the file
# since it defines a few module-wide variables
try:
    from compas_bootstrapper import PYTHON_DIRECTORY
except:  # noqa: E722
    # We re-map CONDA_PREFIX for backwards compatibility reasons
    # In a few releases down the line, we can get rid of this bit
    try:
        from compas_bootstrapper import CONDA_PREFIX as PYTHON_DIRECTORY
    except:  # noqa: E722
        PYTHON_DIRECTORY = None

try:
    from compas_bootstrapper import CONDA_EXE
except:  # noqa: E722
    CONDA_EXE = None


def select_python(python_executable):
    """Selects the most likely python interpreter to run.

    This function detects if there is a conda environment we can use,
    or if we need to default to a system-wide python interpreter instead.

    Parameters
    ----------
    python_executable : str
        Select which python executable you want to use,
        either `python` or `pythonw`.
    """
    if PYTHON_DIRECTORY and os.path.exists(PYTHON_DIRECTORY):
        python_executables = [python_executable] if python_executable else ['pythonw', 'python']

        for python_exe in python_executables:
            python = os.path.join(PYTHON_DIRECTORY, python_exe)
            if os.path.exists(python):
                return python

            python = os.path.join(PYTHON_DIRECTORY, '{0}.exe'.format(python_exe))
            if os.path.exists(python):
                return python

            python = os.path.join(PYTHON_DIRECTORY, 'bin', python_exe)
            if os.path.exists(python):
                return python

            python = os.path.join(PYTHON_DIRECTORY, 'bin', '{0}.exe'.format(python_exe))
            if os.path.exists(python):
                return python

    default_exe = 'pythonw' if is_windows() else 'python'

    # Assume a system-wide install exists
    return python_executable or default_exe


def prepare_environment(env=None):
    """Prepares an environment context to run Python on.

    If Python is being used from a conda environment, this is roughly equivalent
    to activating the conda environment by setting up the correct environment
    variables.

    Parameters
    ----------
    env : dict, optional
        Dictionary of environment variables to modify. If ``None`` is passed, then
        this will create a copy of the current ``os.environ``.

    Returns
    -------
    dict
        Updated environment variable dictionary.
    """

    if env is None:
        env = os.environ.copy()

    if PYTHON_DIRECTORY:
        if is_windows():
            lib_bin = os.path.join(PYTHON_DIRECTORY, 'Library', 'bin')
        else:
            lib_bin = os.path.join(PYTHON_DIRECTORY, 'bin')

        if os.path.exists(lib_bin) and lib_bin not in env['PATH']:
            env['PATH'] = lib_bin + os.pathsep + env['PATH']

    if CONDA_EXE:
        env['CONDA_EXE'] = CONDA_EXE

    return env


def absjoin(*parts):
    return os.path.abspath(os.path.join(*parts))


# Cache whatever symlink function works (native or polyfill)
_os_symlink = None


def _polyfill_symlinks(symlinks, raise_on_error):
    """Create multiple symlinks using the polyfill implementation."""
    _handle, temp_path = tempfile.mkstemp(suffix='.cmd', text=True)

    with open(temp_path, 'w') as mklink_cmd:
        mklink_cmd.write('@echo off\n')
        mklink_cmd.write('SET /A symlink_result=0\n')
        mklink_cmd.write('ECHO ret=%symlink_result%\n')
        for i, (source, link_name) in enumerate(symlinks):
            dir_symlink_arg = '/D' if os.path.isdir(source) else ''
            mklink_cmd.write("mklink {} {}\n".format(dir_symlink_arg, subprocess.list2cmdline([link_name, source])))
            mklink_cmd.write('IF %ERRORLEVEL% EQU 0 SET /A symlink_result += {} \n'.format(2 ** i))

        mklink_cmd.write('EXIT /B %symlink_result%\n')

    ret_value = _run_as_admin([temp_path])

    # The error level integer (ret_value) reflects the success/failure of
    # each of the symlink operations, so we do a bit of bitwise-arithmetic
    # on it to figure out which symlinks worked and which ones failed.

    result = []
    for i in range(len(symlinks)):
        success = ret_value & 2 ** i != 0
        result.append(success)

    return result


def _native_symlinks(symlinks, raise_on_error):
    """Create multiple symlinks using the native implementation."""
    result = []

    for source, link_name in symlinks:
        try:
            os.symlink(source, link_name)
            result.append(True)
        except OSError:
            if raise_on_error:
                raise

            result.append(False)

    return result


def _set_symlink_function(fn):
    global _os_symlink
    _os_symlink = fn


def _get_symlink_function():
    global _os_symlink
    allow_polyfill_retry = False

    if not _os_symlink:
        if getattr(os, 'symlink', None):
            _os_symlink = _native_symlinks

        if is_windows():
            if not callable(_os_symlink):
                _os_symlink = _polyfill_symlinks
            else:
                allow_polyfill_retry = True

    return _os_symlink, allow_polyfill_retry


def create_symlink(source, link_name):
    """Create a symbolic link pointing to source named link_name.

    Parameters
    ----------
    source: str
        Source of the link
    link_name: str
        Link name.

    Notes
    -----
    This function is a polyfill of the native ``os.symlink``
    for Python 2.x on Windows platforms.
    """
    create_symlinks([(source, link_name)], raise_on_error=True)


def create_symlinks(symlinks, raise_on_error=False):
    """Create multiple symbolic links in one call.

    Parameters
    ----------
    symlinks: list[str]ing tuples
        List of ``source`` and ``link_name`` of the symlinks as tuples.
    """
    symlink, allow_polyfill_retry = _get_symlink_function()

    try:
        return symlink(symlinks, raise_on_error=True)
    except OSError:
        if raise_on_error:
            raise

        _set_symlink_function(_polyfill_symlinks)
        return _polyfill_symlinks(symlinks, raise_on_error=False)


def remove_symlink(symlink):
    """Remove a symlink from the file system.

    Parameters
    ----------
    symlink : str
        Symlink to remove.
    """
    # Broken links return False on .exists(), so we need to check .islink() as well
    if not (os.path.islink(symlink) or os.path.exists(symlink)):
        return

    if os.path.isdir(symlink):
        try:
            os.rmdir(symlink)
        except NotADirectoryError:
            os.unlink(symlink)
        except PermissionError:
            if not is_windows():
                raise

            _run_command_as_admin('rmdir', [symlink])
    else:
        os.unlink(symlink)


def remove_symlinks(symlinks, raise_on_error=False):
    """Remove one or more symlinks.

    Parameters
    ----------
    links
        Sequence of symlinks to remove.
    raise_on_error : bool
        ``False`` to continue removing even on error,
        otherwise ``True``.

    Returns
    -------
    list
        If ``raise_on_error`` is ``False``, returns a list
        of bools indicating which links were successfully removed.
    """
    result = []

    for symlink in symlinks:
        try:
            remove_symlink(symlink)
            result.append(True)
        except OSError:
            if raise_on_error:
                raise

            result.append(False)

    return result


def rename(src, dst):
    """Rename a file or directory."""
    try:
        os.rename(src, dst)
    except (PermissionError, OSError):
        if not is_windows():
            raise

        _run_command_as_admin('move', [src, dst])


def remove(path):
    """Remove path."""
    try:
        os.remove(path)
    except (PermissionError, OSError):
        if not is_windows():
            raise

        _run_command_as_admin('del', [path])


def copy(src, dst):
    """Copy a file from source to destination."""
    try:
        shutil.copy(src, dst)
    except (PermissionError, OSError):
        if not is_windows():
            raise

        _run_command_as_admin('copy', [src, dst])


def is_admin():
    """Determines whether the current user has admin rights.

    Returns
    -------
    bool
        True if the user is administrator, otherwise False.
    """
    if not is_windows():
        return os.getuid() == 0

    try:
        return bool(ctypes.windll.shell32.IsUserAnAdmin())
    except:  # noqa: E722
        return False


def _run_command_as_admin(command, arguments):
    """Run a single command as admin on Windows.

    Parameters
    ----------
    command : str
        Command name.
    arguments : list[str]
        List of arguments.
    """
    _handle, temp_path = tempfile.mkstemp(suffix='.cmd', text=True)

    with open(temp_path, 'w') as remove_symlink_cmd:
        remove_symlink_cmd.write('@echo off\n')
        remove_symlink_cmd.write('{} {}\n'.format(command, subprocess.list2cmdline(arguments)))

    _run_as_admin([temp_path])


def _run_as_admin(command):
    """Run the specified command as an admin.

    Paramters
    ---------
    command : list[str]
        List of strings of the command to run.

    Returns
    -------
    int
        Exit code of the process.
    """

    if not is_windows():
        raise RuntimeError('Only supported on Windows')

    command_file, command_args = command[0], command[1:]

    params = ShellExecuteInfo(
        nShow=int(False),
        fMask=SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
        lpVerb=b'runas',
        lpFile=command_file.encode('cp1252'),
        lpParameters=subprocess.list2cmdline(command_args).encode('cp1252'))

    if not ctypes.windll.shell32.ShellExecuteExA(ctypes.byref(params)):
        raise RuntimeError('Failed to run command "%s" as admin', command_file)

    process_handle = params.hProcess
    ctypes.windll.kernel32.WaitForSingleObject(process_handle, INFINITE)

    ret = ctypes.wintypes.DWORD()
    if ctypes.windll.kernel32.GetExitCodeProcess(process_handle, ctypes.byref(ret)) == 0:
        raise RuntimeError('Failed to retrieve exit code')

    return ret.value


# The following methods has been adapted from the appdirs package
#
# MIT license
# Copyright (c) 2005-2010 ActiveState Software Inc.
#
# Copyright (c) 2013 Eddy Petrișor
# http://github.com/ActiveState/appdirs


def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
    r"""Return full path to the user-specific data dir for this application.

    Parameters
    ----------
    appname : str
        Name of application.
        If ``None``, just the system directory is returned.
    appauthor : str
        Only used on Windows. Name of the appauthor or distributing body
        for this application. Typically it is the owning company name.
        This falls back to appname. You may pass ``False`` to disable it.
    version : str
        Version path element to append to the path.
        You might want to use this if you want multiple versions
        of your app to be able to run independently. If used, this
        would typically be "<major>.<minor>".
        Only applied when appname is present.
    roaming : bool, optional
        True to use the Windows roaming appdata directory, otherwise False.
        That means that for users on a Windows network setup for roaming profiles,
        this user data will be sync'd on login. See
        <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
        for a discussion of issues.

    Returns
    -------
    str
        Full path to the user-specific data dir.
    """
    if is_windows():
        if appauthor is None:
            appauthor = appname
        const = "CSIDL_APPDATA" if roaming else "CSIDL_LOCAL_APPDATA"
        path = os.path.normpath(_get_win_folder(const))
        if appname:
            if appauthor is not False:
                path = os.path.join(path, appauthor, appname)
            else:
                path = os.path.join(path, appname)

    elif is_osx():
        path = os.path.expanduser('~/Library/Application Support/')
        if appname:
            path = os.path.join(path, appname)

    elif is_mono():
        path = os.path.expanduser('~/Library/Application Support/')
        if appname:
            path = os.path.join(path, appname)

    else:
        # is_linux()
        path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
        if appname:
            path = os.path.join(path, appname)

    if appname and version:
        path = os.path.join(path, version)

    return path


def _get_win_folder_from_registry(csidl_name):
    """This is a fallback technique at best. I'm not sure if using the
    registry for this guarantees us the correct answer for all CSIDL_*
    names.
    """
    if PY3:
        import winreg as _winreg
    else:
        import _winreg

    shell_folder_name = {
        "CSIDL_APPDATA": "AppData",
        "CSIDL_COMMON_APPDATA": "Common AppData",
        "CSIDL_LOCAL_APPDATA": "Local AppData",
    }[csidl_name]

    key = _winreg.OpenKey(
        _winreg.HKEY_CURRENT_USER,
        r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
    )
    dir, type = _winreg.QueryValueEx(key, shell_folder_name)
    return dir


def _get_win_folder_with_pywin32(csidl_name):
    from win32com.shell import shellcon, shell
    dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
    # Try to make this a unicode path because SHGetFolderPath does
    # not return unicode strings when there is unicode data in the
    # path.
    try:
        dir = str(dir) if PY3 else unicode(dir)  # noqa: F821

        # Downgrade to short path name if have highbit chars. See
        # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
        has_high_char = False
        for c in dir:
            if ord(c) > 255:
                has_high_char = True
                break
        if has_high_char:
            try:
                import win32api
                dir = win32api.GetShortPathName(dir)
            except ImportError:
                pass
    except UnicodeError:
        pass
    return dir


def _get_win_folder_with_ctypes(csidl_name):
    csidl_const = {
        "CSIDL_APPDATA": 26,
        "CSIDL_COMMON_APPDATA": 35,
        "CSIDL_LOCAL_APPDATA": 28,
    }[csidl_name]

    buf = ctypes.create_unicode_buffer(1024)
    ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)

    # Downgrade to short path name if have highbit chars. See
    # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
    has_high_char = False
    for c in buf:
        if ord(c) > 255:
            has_high_char = True
            break
    if has_high_char:
        buf2 = ctypes.create_unicode_buffer(1024)
        if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
            buf = buf2

    return buf.value


if is_windows():
    try:
        import win32com.shell  # noqa: F401
        _get_win_folder = _get_win_folder_with_pywin32
    except ImportError:
        try:
            from ctypes import windll  # noqa: F401
            _get_win_folder = _get_win_folder_with_ctypes
        except ImportError:
            _get_win_folder = _get_win_folder_from_registry


# src/compas/base.py

"""
********************************************************************************
base
********************************************************************************

.. deprecated:: 1.5
    Use `compas.data` instead

.. currentmodule:: compas.base

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    Base

"""
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import warnings

from compas.data import Data

Base = Data

__all__ = [
    'Base',
]

warnings.warn(
    "The base module is deprecated. Use the data module instead",
    DeprecationWarning,
    stacklevel=2
)


# src/compas/plugins.py

# The COMPAS plugin system owes a lot to pluggy, the pytest plugin framework
# There are portions of code loosely based on pluggy's
# and while it is not strictly derivative work, we include
# their license and copyright notice to give credit where credit is due.
#
# MIT license
# Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
#
# https://github.com/pytest-dev/pluggy
"""
********************************************************************************
plugins
********************************************************************************

.. currentmodule:: compas.plugins

COMPAS has an extensible architecture based on plugins that allows to
customize and extend the functionality of the core framework.

Check out the developer guide to :ref:`plugins` for additional details.

Decorators
==========

.. autosummary::
    :toctree: generated/
    :nosignatures:

    pluggable
    plugin

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    PluginManager

Exceptions
==========

.. autosummary::
    :toctree: generated/
    :nosignatures:

    IncompletePluginImplError
    PluginNotInstalledError
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import functools
import inspect
import pkgutil
import threading

__all__ = [
    'pluggable',
    'plugin',
    'plugin_manager',
    'IncompletePluginImplError',
    'PluginManager',
    'PluginNotInstalledError',
    'PluginValidator',
]


class PluginNotInstalledError(Exception):
    """Exception raised when an extension point is invoked but no plugin is available."""
    pass


def _get_extension_point_url_from_name(domain, category, pluggable_name):
    """Get the extension point URL based on a pluggable method name"""
    return '{}/{}/{}'.format(domain, category, pluggable_name).replace('//', '/')


def _get_extension_point_url_from_method(domain, category, plugin_method):
    """Get the extension point URL based on a method instance"""
    name = getattr(plugin_method, '__name__', None) or str(id(plugin_method))
    return '{}/{}/{}'.format(domain, category, name).replace('//', '/')


class IncompletePluginImplError(Exception):
    """Exception raised when a plugin does not have implementations for all abstract methods of its base class."""
    pass


class PluginImpl(object):
    """Internal data class to keep track of a loaded plugin implementation.

    Parameters
    ----------
    plugin : module
        Instance of the module containing one or more plugin implementations.
    method : method
        Method implementing the a plugin's behavior.
    plugin_opts : dict
        Dictionary containing plugin options.
    """

    def __init__(self, plugin, method, plugin_opts):
        self.plugin = plugin
        self.method = method
        self.opts = plugin_opts

        if plugin_opts['tryfirst']:
            self.key = 1
        elif plugin_opts['trylast']:
            self.key = 3
        else:
            self.key = 2

    @property
    def id(self):
        """Identifier of the plugin implementation."""
        return '{}.{}'.format(self.plugin.__name__, self.method.__name__)

    def __repr__(self):
        return '<PluginImpl id={}, plugin_module={}>'.format(self.id, self.plugin)


class PluginManager(object):
    """Plugin Manager handles discovery and registry of plugins.

    Usually there is only one instance of a plugin manager per host.
    """
    DEBUG = False

    def __init__(self):
        self.importer = Importer()
        self._registry = {}
        self._discovery_done = False
        self._discovery_lock = threading.Lock()

    @property
    def registry(self):
        """Plugin registry.

        Lazy-loaded dictionary of all plugins available in the system.

        Returns
        -------
        dict
            Dictionary of available plugins. The keys are extension point URLs
            and the values are instances of :class:`PluginImpl`.
        """
        if not self._discovery_done:
            self.load_plugins()

        return self._registry

    def load_plugins(self):
        """Load available plugin modules.

        Returns
        -------
        int
            Number of loaded plugins.
        """
        # Since we modify global state,
        # let's lock around this.
        with self._discovery_lock:
            count = 0

            modules = [
                module_name
                for _importer, module_name, is_pkg in pkgutil.iter_modules()
                if is_pkg and module_name.startswith('compas')
            ]

            modules_to_inspect = dict()

            for module_name in modules:
                module = self.importer.try_import(module_name)
                if module:
                    modules_to_inspect[module_name] = module
                else:
                    if self.DEBUG:
                        print('Error importing module {}, skipping entire package.'.format(module_name))
                    continue

                if '__all_plugins__' in dir(module):
                    for plugin_module_name in module.__all_plugins__:
                        plugin_module = self.importer.try_import(plugin_module_name)
                        if plugin_module:
                            modules_to_inspect[plugin_module_name] = plugin_module
                        else:
                            if self.DEBUG:
                                print('Error importing plugin {}, skipping.'.format(plugin_module_name))

            if self.DEBUG:
                print('Will inspect modules: {}'.format(list(modules_to_inspect.keys())))

            for plugin_module in modules_to_inspect.values():
                count += self.register_module(plugin_module)

            self._discovery_done = True

        return count

    def register_module(self, plugin_module):
        """Register a module that potentially contains plugin implementations.

        Parameters
        ----------
        plugin_module : module
            Module instance to inspect for plugins.

        Returns
        -------
        int
            Count of successfully registered plugins in the module.
        """
        count = 0

        # Iterate over the plugin to locate specific @plugin decorated methods
        for name in dir(plugin_module):
            plugin_method = getattr(plugin_module, name)
            plugin_opts = self._parse_plugin_opts(plugin_method)

            if plugin_opts is not None:
                plugin_impl = PluginImpl(plugin_module, plugin_method, plugin_opts)
                plugins_list = self._registry.setdefault(plugin_opts['extension_point_url'], [])
                plugins_list.append(plugin_impl)
                plugins_list.sort(key=lambda p: p.key)

                if self.DEBUG:
                    print('Registered plugin with ID "{}" for extension point: {}'.format(plugin_impl.id, plugin_opts['extension_point_url']))
                count += 1

        return count

    def _parse_plugin_opts(self, plugin_method):
        if not inspect.isroutine(plugin_method):
            return
        try:
            res = getattr(plugin_method, '__plugin_spec__', None)
        except Exception:
            res = {}
        if res is not None and not isinstance(res, dict):
            # false positive
            res = None
        return res


def pluggable(pluggable_method=None, category=None, selector='first_match', domain='https://plugins.compas.dev/'):
    """Decorator to mark a method as a pluggable extension point.

    A pluggable interface is uniquely identifiable/locatable via a URL
    derived from the parameters ``domain``, ``category`` and name ``pluggable_method``.
    In the example below, the URL of the pluggable definition is::

        https://plugins.compas.dev/triangulation/triangulate_mesh

    Parameters
    ----------
    pluggable_method : callable
        The method to decorate as ``pluggable``.
    category : str, optional
        An optional string to group or categorize extension points.
    selector : str, optional
        String that determines the selection mode of extension points.

        - ``"first_match"``: (str) Execute the first matching implementation.
        - ``"collect_all"``: (str) Executes all matching implementations and return list of its return values.

    domain : str, optional
        Domain name that "owns" the pluggable extension point.
        This is useful to avoid name collisions between extension points
        of different packages.

    Examples
    --------
    >>> @pluggable(category='triangulation')
    ... def triangulate_mesh(mesh):
    ...    pass
    """
    def pluggable_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            extension_point_url = _get_extension_point_url_from_method(domain, category, func)

            # Select first matching plugin
            if selector == 'first_match':
                plugin_impl = _select_plugin(extension_point_url)

                # Invoke plugin
                return plugin_impl.method(*args, **kwargs)

            # Collect all matching plugins
            elif selector == 'collect_all':
                results = []

                for plugin_impl in _collect_plugins(extension_point_url):
                    try:
                        result = plugin_impl.method(*args, **kwargs)
                        results.append(result)
                    except Exception as e:
                        results.append(e)

                return results
            else:
                raise ValueError('Unexpected selector type. Must be either: first_match or collect_all')

        return wrapper

    if pluggable_method is None:
        return pluggable_decorator
    else:
        return pluggable_decorator(pluggable_method)


def plugin(method=None, category=None, requires=None, tryfirst=False, trylast=False, pluggable_name=None, domain='https://plugins.compas.dev/'):
    """Decorator to declare a plugin.

    A plugin decorator marks a method as a plugin for a specified
    :meth:`pluggable` extension point. Plugins are matched to their pluggable
    counterparts by a combination of the name of the plugin method, the category
    and the domain specified. These 3 parts form the **extension point URL** used
    for matching.

    Parameters
    ----------
    method : callable
        The method to decorate as ``plugin``.
    category : str, optional
        An optional string to group or categorize plugins.
    requires : list, optional
        Optionally defines a list of requirements that should be fulfilled
        for this plugin to be used. The requirement can either be a package
        name (``str``) or a ``callable`` with a boolean return value,
        in which any arbitrary check can be implemented.
    tryfirst : bool, optional
        Plugins can declare a preferred priority by setting this to ``True``.
        By default ``False``.
    trylast : bool, optional
        Alternatively, a plugin can demote itself to be least preferable
        setting ``trylast`` to ``True``. By default ``False``.
    pluggable_name : str, optional
        Usually, the name of the decorated plugin method matches that of the
        pluggable interface. When that is not the case, the pluggable name can be
        specified via this parameter.
    domain : str, optional
        Domain name that "owns" the pluggable extension point.
        This is useful to disambiguate name collisions between extension points
        of different packages.
    """
    def setattr_hookspec_opts(func):
        if tryfirst and trylast:
            raise ValueError('You cannot set a plugin to try first and last at the same time.')

        name = pluggable_name or getattr(func, '__name__', None)
        extension_point_url = _get_extension_point_url_from_name(domain, category, name)

        setattr(
            func,
            '__plugin_spec__',
            dict(
                extension_point_url=extension_point_url,
                pluggable_name=name,
                requires=requires,
                tryfirst=tryfirst,
                trylast=trylast,
            ),
        )
        return func

    if method is not None:
        return setattr_hookspec_opts(method)
    else:
        return setattr_hookspec_opts


class Importer(object):
    """Internal class to help importing modules."""

    def __init__(self):
        # dictionary of module_name => bool (importable yes/no)
        self._cache = {}

    def try_import(self, module_name):
        """Attempt to import a module, but do not raise in case of error.

        Parameters
        ----------
        module_name : str
            Module to try to import.

        Returns
        -------
        module
            If importable, it returns the imported module, otherwise ``None``.
        """
        module = None

        try:
            module = __import__(module_name, fromlist=['__name__'], level=0)
            self._cache[module_name] = True

        # There are two types of possible failure modes:
        # 1) cannot be imported, or
        # 2) is a python 3 module and we're in IPY, which causes a SyntaxError
        except (ImportError, SyntaxError):
            self._cache[module_name] = False

        return module

    def check_importable(self, module_name):
        """Check if a module is importable.

        Parameters
        ----------
        module_name : str
            Name of the module to check for importability.

        Returns
        -------
        bool
            ``True`` if the module can be imported correctly, otherwise ``False``.
        """
        if module_name not in self._cache:
            self.try_import(module_name)

        return self._cache[module_name]


class PluginValidator(object):
    """Plugin Validator handles validation of plugins."""

    def __init__(self, manager):
        self.manager = manager

    def verify_requirement(self, requirement):
        if callable(requirement):
            return requirement()

        return self.manager.importer.check_importable(requirement)

    def is_plugin_selectable(self, plugin):
        if plugin.opts['requires']:
            importable_requirements = (self.verify_requirement(requirement) for requirement in plugin.opts['requires'])

            if not all(importable_requirements):
                if self.manager.DEBUG:
                    print('Requirements not satisfied. Plugin will not be used: {}'.format(plugin.id))
                return False

        return True

    def select_plugin(self, extension_point_url):
        if self.manager.DEBUG:
            print('Extension Point URL {} invoked. Will select a matching plugin'.format(extension_point_url))

        plugins = self.manager.registry.get(extension_point_url) or []
        for plugin in plugins:
            if self.is_plugin_selectable(plugin):
                return plugin

        # Nothing found, raise
        raise PluginNotInstalledError('Plugin not found for extension point URL: {}'.format(extension_point_url))

    def collect_plugins(self, extension_point_url):
        if self.manager.DEBUG:
            print('Extension Point URL {} invoked. Will select a matching plugin'.format(extension_point_url))

        plugins = self.manager.registry.get(extension_point_url) or []
        return [plugin for plugin in plugins if self.is_plugin_selectable(plugin)]

    @staticmethod
    def ensure_implementations(cls):
        for name, value in inspect.getmembers(cls):
            if inspect.isfunction(value) or inspect.ismethod(value):
                if hasattr(value, '__isabstractmethod__'):
                    raise IncompletePluginImplError('Abstract method not implemented: {}'.format(value))


plugin_manager = PluginManager()
_select_plugin = PluginValidator(plugin_manager).select_plugin
_collect_plugins = PluginValidator(plugin_manager).collect_plugins


# src/compas_blender/__init__.py

"""
********************************************************************************
compas_blender
********************************************************************************

.. currentmodule:: compas_blender

.. toctree::
    :maxdepth: 1

    compas_blender.artists
    compas_blender.conversions
    compas_blender.geometry
    compas_blender.ui
    compas_blender.utilities

"""
import os
import compas

try:
    import bpy  # noqa: F401
except ImportError:
    pass
else:
    from .utilities import *  # noqa: F401 F403


def clear():
    """Clear all scene objects."""
    # delete all objects
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=True, confirm=False)
    # delete data
    delete_unused_data()  # noqa: F405
    # delete collections
    for collection in bpy.context.scene.collection.children:
        bpy.context.scene.collection.children.unlink(collection)
    for block in bpy.data.collections:
        objects = [o for o in block.objects if o.users]
        while objects:
            bpy.data.objects.remove(objects.pop())
        for collection in block.children:
            block.children.unlink(collection)
        if block.users == 0:
            bpy.data.collections.remove(block)


def redraw():
    """Trigger a redraw."""
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)


__version__ = '1.13.3'


def _check_blender_version(version):
    supported_versions = ['2.83', '2.93']

    if not version:
        return '2.93'

    if version not in supported_versions:
        raise Exception('Unsupported Blender version: {}'.format(version))

    return version


def _get_default_blender_installation_path(version):
    version = _check_blender_version(version)

    if compas.OSX:
        path = _get_default_blender_installation_path_mac(version)
    elif compas.WINDOWS:
        path = _get_default_blender_installation_path_windows(version)
    else:
        raise Exception('Unsupported platform.')

    if not os.path.exists(path):
        raise Exception("The default installation folder for Blender {} doesn't exist.".format(version))

    return path


def _get_default_blender_installation_path_mac(version):
    return '/Applications/Blender.app/Contents/Resources/{}'.format(version)


def _get_default_blender_installation_path_windows(version):
    return os.path.expandvars('%PROGRAMFILES%/Blender Foundation/Blender {}/{}'.format(version, version))


__all__ = [name for name in dir() if not name.startswith('_')]

__all_plugins__ = [
    'compas_blender.geometry.booleans',
    'compas_blender.artists',
]


# src/compas_blender/install.py

import os
import sys
import tempfile

import compas
import compas_blender

from compas._os import copy
from compas._os import create_symlinks
from compas._os import remove
from compas._os import remove_symlink
from compas._os import rename


__all__ = ['install']


BOOTSTRAPPER_TEMPLATE = """
import os
import sys

import bpy

ENVIRONMENT_NAME = r"{}"
PYTHON_DIRECTORY = r"{}"

PREVIOUS_ENVIRON = os.environ.copy()

# Blender uses `register` and `unregister` functions now for the startup scripts
def register():
    sys.version = '{}'

    # We delay the import of compas._os because
    # it depends this very same compas_bootstrapper
    import compas._os

    # Update current env variables
    compas._os.prepare_environment(os.environ)

def unregister():
    # Restore environment state to before registration
    os.environ = PREVIOUS_ENVIRON

"""


def install(blender_path, version=None):
    """Install COMPAS for Blender.

    Parameters
    ----------
    blender_path : str
        The path to the folder with the version number of Blender.
        For example, on Mac: ``'/Applications/Blender.app/Contents/Resources/2.83'``.
        On Windows: ``'%PROGRAMFILES%/Blender Foundation/Blender 2.83/2.83'``.
    version : {'2.83', '2.93'}, optional
        The version number of Blender.
        Default is ``'2.93'``.

    Examples
    --------
    .. code-block:: bash

        $ python -m compas_blender.install

    .. code-block:: bash

        $ python -m compas_blender.install -v 2.93

    .. code-block:: bash

        $ python -m compas_blender.install /Applications/Blender.app/Contents/Resources/2.93

    """
    if not os.environ.get('CONDA_PREFIX'):
        print('Conda environment not found. The installation into Blender requires an active conda environment with a matching Python version to continue.')
        sys.exit(-1)

    if not version and not blender_path:
        version = '2.93'

    if version and blender_path:
        print('Both options cannot be provided simultaneously. Provide the full installation path, or the version with flag -v.')
        sys.exit(-1)

    if version:
        if compas.LINUX:
            print('Version-based installs are currently not supported for Linux. Please provide the full installation path with the -p option.')
            sys.exit(-1)

        blender_path = compas_blender._get_default_blender_installation_path(version)

    if not os.path.exists(blender_path):
        raise FileNotFoundError('Blender version folder not found.')

    path, version = os.path.split(blender_path)

    print('Installing COMPAS for Blender {}'.format(version))

    startup = os.path.join(blender_path, 'scripts', 'startup')

    blenderpython_src = os.path.join(blender_path, 'python')
    blenderpython_dst = os.path.join(blender_path, 'original_python')
    compas_bootstrapper = os.path.join(startup, 'compas_bootstrapper.py')

    if os.path.exists(blenderpython_dst):
        print('Found existing installation, restoring bundled python installation...')
        if os.path.exists(blenderpython_src):
            remove_symlink(blenderpython_src)

        print('  Renaming original_python back to bundled python folder: {} => {}'.format(blenderpython_dst, blenderpython_src))
        rename(blenderpython_dst, blenderpython_src)

    if os.path.exists(compas_bootstrapper):
        print('  Removing compas bootstrapper...')
        remove(compas_bootstrapper)

    print('Installing current conda environment into Blender...')
    print('  Renaming bundled python folder to original_python: {} => {}'.format(blenderpython_src, blenderpython_dst))
    rename(blenderpython_src, blenderpython_dst)
    create_symlinks([(os.environ['CONDA_PREFIX'], blenderpython_src)])

    # Take either the CONDA environment directory or the current Python executable's directory
    python_directory = os.environ.get('CONDA_PREFIX', None) or os.path.dirname(sys.executable)
    environment_name = os.environ.get('CONDA_DEFAULT_ENV', '')

    # Get current sys.version value, we will override it inside Blender
    # because it seems Blender overrides it as well, but doing so breaks many things after having replaced the Python interpreter
    sys_version = "\\n".join(sys.version.split("\n"))

    _handle, bootstrapper_temp_path = tempfile.mkstemp(suffix='.py', text=True)

    with open(bootstrapper_temp_path, 'w') as f:
        f.write(BOOTSTRAPPER_TEMPLATE.format(environment_name, python_directory, sys_version))

    print('  Creating bootstrap script: {}'.format(compas_bootstrapper))
    copy(bootstrapper_temp_path, compas_bootstrapper)

    print()
    print('COMPAS for Blender {} has been installed.'.format(version))


# ==============================================================================
# Main
# ==============================================================================

if __name__ == "__main__":

    import argparse

    parser = argparse.ArgumentParser()

    parser.add_argument('blenderpath', nargs='?', help="The path to the folder with the version number of Blender.")
    parser.add_argument('-v', '--version', choices=['2.83', '2.93'], help="The version of Blender to install COMPAS in.")

    args = parser.parse_args()

    install(args.blenderpath, version=args.version)


# src/compas_blender/uninstall.py

import os
import sys

from compas._os import remove
from compas._os import remove_symlink
from compas._os import rename

__all__ = ['uninstall']


def uninstall(blender_path):
    """Uninstall COMPAS from Blender.

    Parameters
    ----------
    blender_path : str
        The path to the folder with the version number of Blender.
        For example, on Mac: ``'/Applications/blender.app/Contents/Resources/2.80'``.
        On Windows: ``'%PROGRAMFILES%\\Blender Foundation\\Blender\\2.80'``.

    Examples
    --------
    .. code-block:: bash

        $ python -m compas_blender.uninstall /Applications/blender.app/Contents/Resources/2.80

    """
    if not os.environ.get('CONDA_PREFIX'):
        print('Conda environment not found. The installation into Blender requires an active conda environment with a matching Python version to continue.')
        sys.exit(-1)

    path, version = os.path.split(blender_path)

    print('Uninstalling COMPAS for Blender {}'.format(version))

    startup = os.path.join(blender_path, 'scripts', 'startup')

    blenderpython_src = os.path.join(blender_path, 'python')
    blenderpython_dst = os.path.join(blender_path, 'original_python')
    compas_bootstrapper = os.path.join(startup, 'compas_bootstrapper.py')

    if os.path.exists(blenderpython_dst):
        print('Found existing installation, restoring bundled python installation...')
        if os.path.exists(blenderpython_src):
            remove_symlink(blenderpython_src)

        print('  Renaming original_python back to bundled python folder: {} => {}'.format(blenderpython_dst, blenderpython_src))
        rename(blenderpython_dst, blenderpython_src)

    if os.path.exists(compas_bootstrapper):
        print('  Removing compas bootstrapper...')
        remove(compas_bootstrapper)

    print()
    print('COMPAS has been uninstalled from Blender {}.'.format(version))


# ==============================================================================
# Main
# ==============================================================================

if __name__ == "__main__":

    import argparse

    parser = argparse.ArgumentParser()

    parser.add_argument('versionpath', help="The path to the folder with the version number of Blender.")
    args = parser.parse_args()

    uninstall(args.versionpath)


# src/compas_ghpython/__init__.py

"""
********************************************************************************
compas_ghpython
********************************************************************************

.. currentmodule:: compas_ghpython

.. toctree::
    :maxdepth: 1

    compas_ghpython.artists
    compas_ghpython.utilities

"""
import io
import os
import urllib
import zipfile

import compas
import compas_rhino

__version__ = '1.13.3'

if compas.is_rhino():
    from .utilities import *  # noqa: F401 F403

__all__ = [
    'get_grasshopper_managedplugin_path',
    'get_grasshopper_library_path',
    'get_grasshopper_userobjects_path',
    'fetch_ghio_lib'
]
__all_plugins__ = [
    'compas_ghpython.install',
    'compas_ghpython.uninstall',
    'compas_ghpython.artists',
]


# =============================================================================
# General Helpers
# =============================================================================


def _get_grasshopper_special_folder(version, folder_name):
    grasshopper = compas_rhino._get_rhino_grasshopperplugin_path(version)
    return os.path.join(grasshopper, folder_name)


# =============================================================================
# Managed Plugin
# =============================================================================


def get_grasshopper_managedplugin_path(version):
    version = compas_rhino._check_rhino_version(version)
    managedplugins = compas_rhino._get_rhino_managedplugins_path(version)

    if compas.WINDOWS:
        gh_managedplugin_path = os.path.join(managedplugins, 'Grasshopper')

    elif compas.OSX:
        gh_managedplugin_path = os.path.join(managedplugins, 'GrasshopperPlugin.rhp')

    if not os.path.exists(gh_managedplugin_path):
        raise Exception("The Grasshopper (managed) Plug-in folder does not exist in this location: {}".format(gh_managedplugin_path))

    return gh_managedplugin_path


# =============================================================================
# GH Plugin Libraries path
# =============================================================================


def get_grasshopper_library_path(version):
    """Retrieve Grasshopper's library (components) path"""
    return _get_grasshopper_special_folder(version, 'Libraries')


# =============================================================================
# GH Plugin UserObjects path
# =============================================================================


def get_grasshopper_userobjects_path(version):
    """Retrieve Grasshopper's user objects path"""
    return _get_grasshopper_special_folder(version, 'UserObjects')


# =============================================================================
# GH_IO Dll
# =============================================================================


def fetch_ghio_lib(target_folder='temp'):
    """Fetch the GH_IO.dll library from the NuGet packaging system."""
    ghio_dll = 'GH_IO.dll'
    filename = 'lib/net48/' + ghio_dll

    response = urllib.request.urlopen('https://www.nuget.org/api/v2/package/Grasshopper/')
    dst_file = os.path.join(target_folder, ghio_dll)
    zip_file = zipfile.ZipFile(io.BytesIO(response.read()))

    with zip_file.open(filename, 'r') as zipped_dll:
        with open(dst_file, 'wb') as fp:
            fp.write(zipped_dll.read())

    return dst_file


# src/compas_ghpython/install.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os

import compas.plugins
from compas_ghpython.components import install_userobjects


@compas.plugins.plugin(category='install')
def installable_rhino_packages():
    return ['compas_ghpython']


@compas.plugins.plugin(category='install')
def after_rhino_install(installed_packages):
    if 'compas_ghpython' not in installed_packages:
        return []

    installed_objects = install_userobjects(os.path.join(os.path.dirname(__file__), 'components', 'ghuser'))

    return [('compas_ghpython', 'Installed {} GH User Objects'.format(len(installed_objects)), True)]


# src/compas_ghpython/uninstall.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import glob
import os

import compas
from compas_ghpython.components import uninstall_userobjects


@compas.plugins.plugin(category='install')
def after_rhino_uninstall(uninstalled_packages):
    if 'compas_ghpython' not in uninstalled_packages:
        return []

    srcdir = os.path.join(os.path.dirname(__file__), 'components')
    userobjects = [os.path.basename(ghuser) for ghuser in glob.glob(os.path.join(srcdir, '*.ghuser'))]
    uninstalled_objects = uninstall_userobjects(userobjects)

    uninstall_errors = [uo[0] for uo in uninstalled_objects if not uo[1]]
    error_msg = '' if not uninstall_errors else 'and {} failed to uninstall'.format(len(uninstall_errors))

    return [('compas_ghpython', 'Uninstalled {} GH User Objects {}'.format(len(uninstalled_objects), error_msg), True)]


# src/compas_plotters/__init__.py

"""
********************************************************************************
compas_plotters
********************************************************************************

.. currentmodule:: compas_plotters

.. toctree::
    :maxdepth: 1
    :titlesonly:

    compas_plotters.artists
    compas_plotters.plotter

"""

__version__ = '1.13.3'

from .core import *  # noqa: F401 F403
# from .artists import *  # noqa: F401 F403
from .plotter import Plotter


__all__ = [
    'Plotter'
]

__all_plugins__ = [
    'compas_plotters.artists',
]


# src/compas_plotters/plotter.py

"""
********************************************************************************
plotter
********************************************************************************

.. currentmodule:: compas_plotters.plotter

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    Plotter

"""

import os
from typing import Callable, Optional, Tuple, List, Union
from typing_extensions import Literal
import matplotlib
import matplotlib.pyplot as plt
import tempfile
from PIL import Image

import compas
from .artists import PlotterArtist


class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


class Plotter:
    """Plotter for the visualization of COMPAS geometry.

    Parameters
    ----------
    view : tuple[tuple[float, float], tuple[float, float]], optional
        The area of the axes that should be zoomed into view.
    figsize : tuple[float, float], optional
        Size of the figure in inches.
    dpi : float, optional
        Resolution of the figure in "dots per inch".
    bgcolor : tuple[float, float, float], optional
        Background color for the figure canvas.
    show_axes : bool, optional
        If True, show the axes of the figure.
    zstack : {'natural', 'zorder'}, optional
        If ``'natural'``, the drawing elements appear in the order they were added.
        If ``'natural'``, the drawing elements are added based on their `zorder`.

    Attributes
    ----------
    viewbox : tuple[tuple[float, float], tuple[float, float]]
        X min-max and Y min-max of the area of the axes that is zoomed into view.
    axes : matplotlib.axes.Axes, read-only
        `matplotlib` axes object used by the figure.
        For more info, see the documentation of the Axes class ([1]_) and the axis and tick API ([2]_).
    figure : matplotlib.figure.Figure, read-only
        `matplotlib` figure instance.
        For more info, see the figure API ([3]_).
    bgcolor : tuple[float, float, float]
        Background color of the figure.
    title : str
        Title of the plot.
    artists : list[:class:`compas_plotters.artists.PlotterArtist`]
        Artists that should be included in the plot.

    Class Attributes
    ----------------
    fontsize : int
        Default fontsize used by the plotter.

    References
    ----------
    .. [1] https://matplotlib.org/api/axes_api.html
    .. [2] https://matplotlib.org/api/axis_api.html
    .. [3] https://matplotlib.org/api/figure_api.html

    Examples
    --------
    >>> from compas.geometry import Point, Plane, Circle
    >>> from compas_plotters.plotter import Plotter

    Create a plotter instance.

    >>> plotter = Plotter()

    Add COMPAS objects.

    >>> plotter.add(Point(0, 0, 0))
    <compas_plotters.artists.pointartist.PointArtist object at 0x17880eb80>
    >>> plotter.add(Circle(Plane.worldXY(), 1.0))
    <compas_plotters.artists.circleartist.CircleArtist object at 0x10d136e80>

    """

    fontsize = 12

    def __init__(self,
                 view: Tuple[Tuple[float, float], Tuple[float, float]] = ((-8.0, 16.0), (-5.0, 10.0)),
                 figsize: Tuple[float, float] = (8.0, 5.0),
                 dpi: float = 100,
                 bgcolor: Tuple[float, float, float] = (1.0, 1.0, 1.0),
                 show_axes: bool = False,
                 zstack: Literal['natural', 'zorder'] = 'zorder'):
        self._show_axes = show_axes
        self._bgcolor = None
        self._viewbox = None
        self._axes = None
        self._artists = []
        self.viewbox = view
        self.figsize = figsize
        self.dpi = dpi
        self.bgcolor = bgcolor
        self.zstack = zstack

    @property
    def viewbox(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
        return self._viewbox

    @viewbox.setter
    def viewbox(self, view: Tuple[Tuple[float, float], Tuple[float, float]]):
        xlim, ylim = view
        xmin, xmax = xlim
        ymin, ymax = ylim
        self._viewbox = (xmin, xmax), (ymin, ymax)

    @property
    def axes(self) -> matplotlib.axes.Axes:
        if not self._axes:
            figure = plt.figure(facecolor=self.bgcolor,
                                figsize=self.figsize,
                                dpi=self.dpi)
            axes = figure.add_subplot(111, aspect='equal')
            if self.viewbox:
                xmin, xmax = self.viewbox[0]
                ymin, ymax = self.viewbox[1]
                axes.set_xlim(xmin, xmax)
                axes.set_ylim(ymin, ymax)
            axes.set_xscale('linear')
            axes.set_yscale('linear')
            if self._show_axes:
                axes.set_frame_on(True)
                axes.grid(False)
                axes.set_xticks([])
                axes.set_yticks([])
                axes.spines['top'].set_color('none')
                axes.spines['right'].set_color('none')
                axes.spines['left'].set_position('zero')
                axes.spines['bottom'].set_position('zero')
                axes.spines['left'].set_linestyle('-')
                axes.spines['bottom'].set_linestyle('-')
            else:
                axes.grid(False)
                axes.set_frame_on(False)
                axes.set_xticks([])
                axes.set_yticks([])
            axes.autoscale_view()
            plt.tight_layout()
            self._axes = axes
        return self._axes

    @property
    def figure(self) -> matplotlib.figure.Figure:
        return self.axes.get_figure()

    @property
    def bgcolor(self) -> str:
        return self._bgcolor

    @bgcolor.setter
    def bgcolor(self, value: Union[str, Tuple[float, float, float]]):
        self._bgcolor = value
        self.figure.set_facecolor(value)

    @property
    def title(self) -> str:
        return self.figure.canvas.get_window_title()

    @title.setter
    def title(self, value: str):
        self.figure.canvas.set_window_title(value)

    @property
    def artists(self) -> List[PlotterArtist]:
        return self._artists

    @artists.setter
    def artists(self, artists: List[PlotterArtist]):
        self._artists = artists

    # =========================================================================
    # Methods
    # =========================================================================

    def pause(self, pause: float) -> None:
        """Pause plotting during the specified interval.

        Parameters
        ----------
        pause: float
            The duration of the pause in seconds.
        """
        if pause:
            plt.pause(pause)

    def zoom_extents(self, padding: Optional[int] = None) -> None:
        """Zoom the view to the bounding box of all objects."""
        padding = padding or 0
        width, height = self.figsize
        fig_aspect = width / height
        data = []
        for artist in self.artists:
            data += artist.data
        x, y = zip(* data)
        xmin = min(x)
        xmax = max(x)
        ymin = min(y)
        ymax = max(y)
        xspan = xmax - xmin + padding
        yspan = ymax - ymin + padding
        data_aspect = xspan / yspan
        xlim = [xmin - 0.1 * xspan, xmax + 0.1 * xspan]
        ylim = [ymin - 0.1 * yspan, ymax + 0.1 * yspan]
        if data_aspect < fig_aspect:
            scale = fig_aspect / data_aspect
            xlim[0] *= scale
            xlim[1] *= scale
        else:
            scale = data_aspect / fig_aspect
            ylim[0] *= scale
            ylim[1] *= scale
        self.viewbox = (xlim, ylim)
        self.axes.set_xlim(*xlim)
        self.axes.set_ylim(*ylim)
        self.axes.autoscale_view()

    def add(self,
            item: Union[compas.geometry.Primitive,
                        compas.datastructures.Network,
                        compas.datastructures.Mesh],
            artist: Optional[PlotterArtist] = None,
            **kwargs) -> PlotterArtist:
        """Add a COMPAS geometry object or data structure to the plot.

        Parameters
        ----------
        item
            A COMPAS geometric primitive, network, or mesh.
        artist
            Type of artist to use for drawing.

        Returns
        -------
        :class:`PlotterArtist`

        """
        if not artist:
            if self.zstack == 'natural':
                zorder = 1000 + len(self._artists) * 100
                artist = PlotterArtist(item, plotter=self, zorder=zorder, context='Plotter', **kwargs)
            else:
                artist = PlotterArtist(item, plotter=self, context='Plotter', **kwargs)
        artist.draw()
        self._artists.append(artist)
        return artist

    # def add_as(self,
    #            item: Union[compas.geometry.Primitive,
    #                        compas.datastructures.Network,
    #                        compas.datastructures.Mesh],
    #            artist_type: PlotterArtist,
    #            **kwargs) -> PlotterArtist:
    #     """Add a COMPAS geometry object or data structure using a specific artist type."""
    #     artist = PlotterArtist(item, artist_type=artist_type, context='Plotter', **kwargs)
    #     artist.draw()
    #     self._artists.append(artist)
    #     return artist

    def add_from_list(self, items, **kwargs) -> List[PlotterArtist]:
        """Add multiple COMPAS geometry objects and/or data structures from a list."""
        artists = []
        for item in items:
            artist = self.add(item, plotter=self, **kwargs)
            artists.append(artist)
        return artists

    def find(self,
             item: Union[compas.geometry.Primitive,
                         compas.datastructures.Network,
                         compas.datastructures.Mesh]) -> PlotterArtist:
        """Find a geometry object or data structure in the plot."""
        for artist in self._artists:
            if item is artist.item:
                return artist

    def register_listener(self, listener: Callable) -> None:
        """Register a listener for pick events.

        Parameters
        ----------
        listener : callable
            The handler for pick events.

        Returns
        -------
        None

        Notes
        -----
        For more information, see the docs of ``mpl_connect`` ([1]_), and on event
        handling and picking ([2]_).

        References
        ----------
        .. [1] https://matplotlib.org/api/backend_bases_api.html#matplotlib.backend_bases.FigureCanvasBase.mpl_connect
        .. [2] https://matplotlib.org/users/event_handling.html

        """
        self.figure.canvas.mpl_connect('pick_event', listener)

    def draw(self, pause: Optional[float] = None) -> None:
        """Draw all objects included in the plot."""
        self.figure.canvas.draw()
        self.figure.canvas.flush_events()
        if pause:
            plt.pause(pause)

    def redraw(self, pause: Optional[float] = None) -> None:
        """Updates and pauses the plot.

        Parameters
        ----------
        pause : float
            Amount of time to pause the plot in seconds.

        """
        for artist in self._artists:
            artist.redraw()
        self.figure.canvas.draw()
        self.figure.canvas.flush_events()
        if pause:
            plt.pause(pause)

    def show(self) -> None:
        """Displays the plot.
        """
        self.draw()
        plt.show()

    def save(self, filepath: str, **kwargs) -> None:
        """Saves the plot to a file.

        Parameters
        ----------
        filepath : str
            Full path of the file.

        Notes
        -----
        For an overview of all configuration options, see [1]_.

        References
        ----------
        .. [1] https://matplotlib.org/2.0.2/api/pyplot_api.html#matplotlib.pyplot.savefig

        """
        plt.savefig(filepath, **kwargs)

    def on(self,
           interval: int = None,
           frames: int = None,
           record: bool = False,
           recording: str = None,
           dpi: int = 150) -> Callable:
        """Method for decorating callback functions in dynamic plots."""
        if record:
            if not recording:
                raise Exception('Please provide a path for the recording.')

        def outer(func: Callable):
            if record:
                with tempfile.TemporaryDirectory() as dirpath:
                    paths = []
                    for f in range(frames):
                        func(f)
                        self.redraw(pause=interval)
                        if record:
                            filepath = os.path.join(dirpath, f'frame-{f}.png')
                            paths.append(filepath)
                            self.save(filepath, dpi=dpi)
                    images = []
                    for path in paths:
                        images.append(Image.open(path))
                    images[0].save(recording, save_all=True, append_images=images[1:], optimize=False, duration=interval * 1000, loop=0)
            else:
                for f in range(frames):
                    func(f)
                    self.redraw(pause=interval)

        return outer


# conftest.py

import pytest
import compas
import math
import numpy


def pytest_ignore_collect(path):
    if "rhino" in str(path):
        return True

    if "blender" in str(path):
        return True

    if "ghpython" in str(path):
        return True

    if "matlab" in str(path):
        return True

    if str(path).endswith('_cli.py'):
        return True


@pytest.fixture(autouse=True)
def add_compas(doctest_namespace):
    doctest_namespace["compas"] = compas


@pytest.fixture(autouse=True)
def add_math(doctest_namespace):
    doctest_namespace["math"] = math


@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
    doctest_namespace["np"] = numpy


# setup.py

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# flake8: noqa
from __future__ import print_function

import io
from os import path

from setuptools import setup


here = path.abspath(path.dirname(__file__))


def read(*names, **kwargs):
    return io.open(
        path.join(here, *names),
        encoding=kwargs.get('encoding', 'utf8')
    ).read()


long_description = read('README.md')
requirements = [r for r in read('requirements.txt').split('\n') if r]
optional_requirements = {}

setup(
    name='COMPAS',
    version='1.13.3',
    description='The COMPAS framework',
    long_description=long_description,
    long_description_content_type='text/markdown',
    url='http://compas.dev',
    author='Tom Van Mele',
    author_email='van.mele@arch.ethz.ch',
    license='MIT',
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'Intended Audience :: Developers',
        'Topic :: Scientific/Engineering',
        'License :: OSI Approved :: MIT License',
        'Operating System :: Unix',
        'Operating System :: POSIX',
        'Operating System :: Microsoft :: Windows',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
        'Programming Language :: Python :: Implementation :: CPython',
    ],
    keywords=['architecture', 'engineering', 'fabrication', 'construction'],
    project_urls={
        "Documentation": "http://compas.dev",
        "Forum": "https://forum.compas-framework.org/",
        "Repository": "https://github.com/compas-dev/compas",
        "Issues": "https://github.com/compas-dev/compas/issues",
    },
    packages=['compas', 'compas_rhino', 'compas_blender', 'compas_ghpython', 'compas_plotters'],
    package_dir={'': 'src'},
    package_data={'compas': ['data/schemas/*.json']},
    data_files=[],
    include_package_data=True,
    zip_safe=False,
    install_requires=requirements,
    python_requires='>=2.7',
    extras_require={
        'planarity': ['planarity'],
    },
    entry_points={
        'console_scripts': [
            'compas_rpc=compas.rpc.__main__:main'
        ]
    },
    ext_modules=[],
    cmdclass={}
)


# tasks.py

# -*- coding: utf-8 -*-
from __future__ import print_function

import contextlib
import glob
import os
import shutil
import sys
import tempfile

from invoke import Exit
from invoke import task

BASE_FOLDER = os.path.dirname(__file__)


class Log(object):
    def __init__(self, out=sys.stdout, err=sys.stderr):
        self.out = out
        self.err = err

    def flush(self):
        self.out.flush()
        self.err.flush()

    def write(self, message):
        self.flush()
        self.out.write(message + '\n')
        self.out.flush()

    def info(self, message):
        self.write('[INFO] %s' % message)

    def warn(self, message):
        self.write('[WARN] %s' % message)


log = Log()


def confirm(question):
    while True:
        response = input(question).lower().strip()

        if not response or response in ('n', 'no'):
            return False

        if response in ('y', 'yes'):
            return True

        print('Focus, kid! It is either (y)es or (n)o', file=sys.stderr)


@task(default=True)
def help(ctx):
    """Lists available tasks and usage."""
    ctx.run('invoke --list')
    log.write('Use "invoke -h <taskname>" to get detailed help for a task.')


@task(help={
    'docs': 'True to clean up generated documentation, otherwise False',
    'bytecode': 'True to clean up compiled python files, otherwise False.',
    'builds': 'True to clean up build/packaging artifacts, otherwise False.'})
def clean(ctx, docs=True, bytecode=True, builds=True, ghuser=True):
    """Cleans the local copy from compiled artifacts."""

    with chdir(BASE_FOLDER):
        if builds:
            ctx.run('python setup.py clean')

        if bytecode:
            for root, dirs, files in os.walk(BASE_FOLDER):
                for f in files:
                    if f.endswith('.pyc'):
                        os.remove(os.path.join(root, f))
                if '.git' in dirs:
                    dirs.remove('.git')

        folders = []

        if docs:
            folders.append('docs/api/generated')

        folders.append('dist/')

        if bytecode:
            for t in ('src', 'tests'):
                folders.extend(glob.glob('{}/**/__pycache__'.format(t), recursive=True))

        if builds:
            folders.append('build/')
            folders.append('src/compas.egg-info/')

        if ghuser:
            folders.append('src/compas_ghpython/components/ghuser')

        for folder in folders:
            shutil.rmtree(os.path.join(BASE_FOLDER, folder), ignore_errors=True)


@task(help={
      'rebuild': 'True to clean all previously built docs before starting, otherwise False.',
      'doctest': 'True to run doctests, otherwise False.',
      'check_links': 'True to check all web links in docs for validity, otherwise False.'})
def docs(ctx, doctest=False, rebuild=False, check_links=False):
    """Builds package's HTML documentation."""

    if rebuild:
        clean(ctx)

    with chdir(BASE_FOLDER):
        if doctest:
            testdocs(ctx, rebuild=rebuild)

        opts = '-E' if rebuild else ''
        ctx.run('sphinx-build {} -b html docs dist/docs'.format(opts))

        if check_links:
            linkcheck(ctx, rebuild=rebuild)


@task()
def lint(ctx):
    """Check the consistency of coding style."""
    log.write('Running flake8 python linter...')
    ctx.run('flake8 src')


@task()
def testdocs(ctx, rebuild=False):
    """Test the examples in the docstrings."""
    log.write('Running doctest...')
    ctx.run('pytest --doctest-modules')


@task()
def linkcheck(ctx, rebuild=False):
    """Check links in documentation."""
    log.write('Running link check...')
    opts = '-E' if rebuild else ''
    ctx.run('sphinx-build {} -b linkcheck docs dist/docs'.format(opts))


@task()
def check(ctx):
    """Check the consistency of documentation, coding style and a few other things."""

    with chdir(BASE_FOLDER):
        lint(ctx)

        log.write('Checking MANIFEST.in...')
        ctx.run('check-manifest')

        log.write('Checking metadata...')
        ctx.run('python setup.py check --strict --metadata')


@task(help={
      'checks': 'True to run all checks before testing, otherwise False.',
      'doctest': 'True to run doctest on all modules, otherwise False.'})
def test(ctx, checks=False, doctest=False):
    """Run all tests."""
    if checks:
        check(ctx)

    with chdir(BASE_FOLDER):
        cmd = ['pytest']
        if doctest:
            cmd.append('--doctest-modules')

        ctx.run(' '.join(cmd))


@task
def prepare_changelog(ctx):
    """Prepare changelog for next release."""
    UNRELEASED_CHANGELOG_TEMPLATE = '## Unreleased\n\n### Added\n\n### Changed\n\n### Removed\n\n\n## '

    with chdir(BASE_FOLDER):
        # Preparing changelog for next release
        with open('CHANGELOG.md', 'r+') as changelog:
            content = changelog.read()
            changelog.seek(0)
            changelog.write(content.replace(
                '## ', UNRELEASED_CHANGELOG_TEMPLATE, 1))

        ctx.run('git add CHANGELOG.md && git commit -m "Prepare changelog for next release"')


@task(help={
      'gh_io_folder': 'Folder where GH_IO.dll is located. If not specified, it will try to download from NuGet.',
      'ironpython': 'Command for running the IronPython executable. Defaults to `ipy`.'})
def build_ghuser_components(ctx, gh_io_folder=None, ironpython=None):
    """Build Grasshopper user objects from source"""
    clean(ctx, docs=False, bytecode=False, builds=False, ghuser=True)
    with chdir(BASE_FOLDER):
        with tempfile.TemporaryDirectory('actions.ghcomponentizer') as action_dir:
            source_dir = os.path.abspath('src/compas_ghpython/components')
            target_dir = os.path.join(source_dir, 'ghuser')
            ctx.run('git clone https://github.com/compas-dev/compas-actions.ghpython_components.git {}'.format(action_dir))

            if not gh_io_folder:
                gh_io_folder = 'temp'
                import compas_ghpython
                compas_ghpython.fetch_ghio_lib(gh_io_folder)

            if not ironpython:
                ironpython = 'ipy'

            gh_io_folder = os.path.abspath(gh_io_folder)
            componentizer_script = os.path.join(action_dir, 'componentize.py')

            ctx.run('{} {} {} {} --ghio "{}"'.format(ironpython, componentizer_script, source_dir, target_dir, gh_io_folder))


@task(help={
      'release_type': 'Type of release follows semver rules. Must be one of: major, minor, patch.'})
def release(ctx, release_type):
    """Releases the project in one swift command!"""
    if release_type not in ('patch', 'minor', 'major'):
        raise Exit('The release type parameter is invalid.\nMust be one of: major, minor, patch')

    # Run checks
    ctx.run('invoke check test')

    # Bump version and git tag it
    ctx.run('bump2version %s --verbose' % release_type)

    # Build project
    ctx.run('python setup.py clean --all sdist bdist_wheel')

    # Prepare changelog for next release
    prepare_changelog(ctx)

    # Clean up local artifacts
    clean(ctx)

    # Upload to pypi
    if confirm('Everything is ready. You are about to push to git which will trigger a release to pypi.org. Are you sure? [y/N]'):
        ctx.run('git push --tags && git push')
    else:
        raise Exit('You need to manually revert the tag/commits created.')


@contextlib.contextmanager
def chdir(dirname=None):
    current_dir = os.getcwd()
    try:
        if dirname is not None:
            os.chdir(dirname)
        yield
    finally:
        os.chdir(current_dir)


# src/compas_rhino/artists/__init__.py

"""
********************************************************************************
artists
********************************************************************************

.. currentmodule:: compas_rhino.artists


Primitive Artists
=================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    CircleArtist
    FrameArtist
    LineArtist
    PlaneArtist
    PointArtist
    PolygonArtist
    PolylineArtist
    VectorArtist


Shape Artists
=============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    BoxArtist
    CapsuleArtist
    ConeArtist
    CylinderArtist
    PolyhedronArtist
    SphereArtist
    TorusArtist


Datastructure Artists
=====================

.. autosummary::
    :toctree: generated/
    :nosignatures:

    MeshArtist
    NetworkArtist
    VolMeshArtist


Robot Artist
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    RobotModelArtist


Base Classes
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    RhinoArtist

"""
from __future__ import absolute_import

from compas.plugins import plugin
from compas.artists import Artist
from compas.artists import ShapeArtist

from compas.geometry import Circle
from compas.geometry import Frame
from compas.geometry import Line
from compas.geometry import Plane
from compas.geometry import Point
from compas.geometry import Polygon
from compas.geometry import Polyline
from compas.geometry import Vector

from compas.geometry import Box
from compas.geometry import Capsule
from compas.geometry import Cone
from compas.geometry import Cylinder
from compas.geometry import Polyhedron
from compas.geometry import Sphere
from compas.geometry import Torus

from compas.datastructures import Mesh
from compas.datastructures import Network
from compas.datastructures import VolMesh

from compas.robots import RobotModel
import compas_rhino

from .artist import RhinoArtist
from .circleartist import CircleArtist
from .frameartist import FrameArtist
from .lineartist import LineArtist
from .planeartist import PlaneArtist
from .pointartist import PointArtist
from .polygonartist import PolygonArtist
from .polylineartist import PolylineArtist
from .vectorartist import VectorArtist
from .boxartist import BoxArtist
from .capsuleartist import CapsuleArtist
from .coneartist import ConeArtist
from .cylinderartist import CylinderArtist
from .polyhedronartist import PolyhedronArtist
from .sphereartist import SphereArtist
from .torusartist import TorusArtist
from .meshartist import MeshArtist
from .networkartist import NetworkArtist
from .volmeshartist import VolMeshArtist
from .robotmodelartist import RobotModelArtist

BaseArtist = RhinoArtist

ShapeArtist.default_color = (255, 255, 255)

MeshArtist.default_color = (0, 0, 0)
MeshArtist.default_vertexcolor = (255, 255, 255)
MeshArtist.default_edgecolor = (0, 0, 0)
MeshArtist.default_facecolor = (255, 255, 255)

NetworkArtist.default_nodecolor = (255, 255, 255)
NetworkArtist.default_edgecolor = (0, 0, 0)

VolMeshArtist.default_color = (0, 0, 0)
VolMeshArtist.default_vertexcolor = (255, 255, 255)
VolMeshArtist.default_edgecolor = (0, 0, 0)
VolMeshArtist.default_facecolor = (255, 255, 255)
VolMeshArtist.default_cellcolor = (255, 0, 0)


@plugin(category='drawing-utils', pluggable_name='clear', requires=['Rhino'])
def clear_rhino():
    compas_rhino.clear()


@plugin(category='drawing-utils', pluggable_name='redraw', requires=['Rhino'])
def redraw_rhino():
    compas_rhino.redraw()


@plugin(category='factories', requires=['Rhino'])
def register_artists():
    Artist.register(Circle, CircleArtist, context='Rhino')
    Artist.register(Frame, FrameArtist, context='Rhino')
    Artist.register(Line, LineArtist, context='Rhino')
    Artist.register(Plane, PlaneArtist, context='Rhino')
    Artist.register(Point, PointArtist, context='Rhino')
    Artist.register(Polygon, PolygonArtist, context='Rhino')
    Artist.register(Polyline, PolylineArtist, context='Rhino')
    Artist.register(Vector, VectorArtist, context='Rhino')
    Artist.register(Box, BoxArtist, context='Rhino')
    Artist.register(Capsule, CapsuleArtist, context='Rhino')
    Artist.register(Cone, ConeArtist, context='Rhino')
    Artist.register(Cylinder, CylinderArtist, context='Rhino')
    Artist.register(Polyhedron, PolyhedronArtist, context='Rhino')
    Artist.register(Sphere, SphereArtist, context='Rhino')
    Artist.register(Torus, TorusArtist, context='Rhino')
    Artist.register(Mesh, MeshArtist, context='Rhino')
    Artist.register(Network, NetworkArtist, context='Rhino')
    Artist.register(VolMesh, VolMeshArtist, context='Rhino')
    Artist.register(RobotModel, RobotModelArtist, context='Rhino')
    print('Rhino Artists registered.')


__all__ = [
    'BaseArtist',
    'RhinoArtist',
    'CircleArtist',
    'FrameArtist',
    'LineArtist',
    'PlaneArtist',
    'PointArtist',
    'PolygonArtist',
    'PolylineArtist',
    'VectorArtist',
    'BoxArtist',
    'CapsuleArtist',
    'ConeArtist',
    'CylinderArtist',
    'PolyhedronArtist',
    'SphereArtist',
    'TorusArtist',
    'MeshArtist',
    'NetworkArtist',
    'VolMeshArtist',
    'RobotModelArtist',
]


# src/compas_rhino/artists/artist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import Artist


class RhinoArtist(Artist):
    """Base class for all Rhino artists.

    Parameters
    ----------
    layer : str, optional
        A layer name.
    **kwargs : dict, optional
        Additional keyword arguments.
        See :class:`Artist` for more info.

    """

    def __init__(self, layer=None, **kwargs):
        super(RhinoArtist, self).__init__(**kwargs)
        self.layer = layer

    def clear_layer(self):
        """Clear the layer of the artist.

        Returns
        -------
        None

        """
        if self.layer:
            compas_rhino.clear_layer(self.layer)


# src/compas_rhino/artists/boxartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import ShapeArtist
from .artist import RhinoArtist


class BoxArtist(RhinoArtist, ShapeArtist):
    """Artist for drawing box shapes.

    Parameters
    ----------
    box : :class:`compas.geometry.Box`
        A COMPAS box.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`.

    """

    def __init__(self, box, layer=None, **kwargs):
        super(BoxArtist, self).__init__(shape=box, layer=layer, **kwargs)

    def draw(self, color=None):
        """Draw the box associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the box.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the objects created in Rhino.

        """
        color = color or self.color
        vertices = [list(vertex) for vertex in self.shape.vertices]
        faces = self.shape.faces
        guid = compas_rhino.draw_mesh(vertices,
                                      faces,
                                      layer=self.layer,
                                      name=self.shape.name,
                                      color=color,
                                      disjoint=True)
        return [guid]


# src/compas_rhino/artists/capsuleartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import ShapeArtist
from .artist import RhinoArtist


class CapsuleArtist(RhinoArtist, ShapeArtist):
    """Artist for drawing capsule shapes.

    Parameters
    ----------
    capsule : :class:`compas.geometry.Capsule`
        A COMPAS capsule.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`.

    """

    def __init__(self, capsule, layer=None, **kwargs):
        super(CapsuleArtist, self).__init__(shape=capsule, layer=layer, **kwargs)

    def draw(self, color=None, u=None, v=None):
        """Draw the capsule associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the capsule.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`~CapsuleArtist.u`.
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`CapsuleArtist.v`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the objects created in Rhino.

        """
        color = color or self.color
        u = u or self.u
        v = v or self.v
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        vertices = [list(vertex) for vertex in vertices]
        guid = compas_rhino.draw_mesh(vertices,
                                      faces,
                                      layer=self.layer,
                                      name=self.shape.name,
                                      color=color,
                                      disjoint=True)
        return [guid]


# src/compas_rhino/artists/circleartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.geometry import add_vectors
from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class CircleArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing circles.

    Parameters
    ----------
    circle : :class:`compas.geometry.Circle`
        A COMPAS circle.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    """

    def __init__(self, circle, layer=None, **kwargs):
        super(CircleArtist, self).__init__(primitive=circle, layer=layer, **kwargs)

    def draw(self, show_point=False, show_normal=False):
        """Draw the circle.

        Parameters
        ----------
        show_point : bool, optional
            If True, draw the center point of the circle.
        show_normal : bool, optional
            If True, draw the normal vector of the circle.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        point = list(self.primitive.plane.point)
        normal = list(self.primitive.plane.normal)
        plane = point, normal
        radius = self.primitive.radius
        guids = []
        if show_point:
            points = [{'pos': point, 'color': self.color, 'name': self.primitive.name}]
            guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        if show_normal:
            lines = [{'start': point, 'end': add_vectors(point, normal), 'arrow': 'end', 'color': self.color, 'name': self.primitive.name}]
            guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)
        circles = [{'plane': plane, 'radius': radius, 'color': self.color, 'name': self.primitive.name}]
        guids += compas_rhino.draw_circles(circles, layer=self.layer, clear=False, redraw=False)
        return guids


# src/compas_rhino/artists/coneartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import ShapeArtist
from .artist import RhinoArtist


class ConeArtist(RhinoArtist, ShapeArtist):
    """Artist for drawing cone shapes.

    Parameters
    ----------
    shape : :class:`compas.geometry.Cone`
        A COMPAS cone.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`.

    """

    def __init__(self, cone, layer=None, **kwargs):
        super(ConeArtist, self).__init__(shape=cone, layer=layer, **kwargs)

    def draw(self, color=None, u=None):
        """Draw the cone associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the cone.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`ConeArtist.u`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the objects created in Rhino.

        """
        color = color or self.color
        u = u or self.u
        vertices, faces = self.shape.to_vertices_and_faces(u=u)
        vertices = [list(vertex) for vertex in vertices]
        guid = compas_rhino.draw_mesh(vertices,
                                      faces,
                                      layer=self.layer,
                                      name=self.shape.name,
                                      color=color,
                                      disjoint=True)
        return [guid]


# src/compas_rhino/artists/cylinderartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import ShapeArtist
from .artist import RhinoArtist


class CylinderArtist(RhinoArtist, ShapeArtist):
    """Artist for drawing cylinder shapes.

    Parameters
    ----------
    cylinder : :class:`compas.geometry.Cylinder`
        A COMPAS cylinder.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`.

    """

    def __init__(self, cylinder, layer=None, **kwargs):
        super(CylinderArtist, self).__init__(shape=cylinder, layer=layer, **kwargs)

    def draw(self, color=None, u=None):
        """Draw the cylinder associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the cylinder.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`CylinderArtist.u`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the objects created in Rhino.

        """
        color = color or self.color
        u = u or self.u
        vertices, faces = self.shape.to_vertices_and_faces(u=u)
        vertices = [list(vertex) for vertex in vertices]
        guid = compas_rhino.draw_mesh(vertices,
                                      faces,
                                      layer=self.layer,
                                      name=self.shape.name,
                                      color=color,
                                      disjoint=True)
        return [guid]


# src/compas_rhino/artists/frameartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class FrameArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing frames.

    Parameters
    ----------
    frame: :class:`compas.geometry.Frame`
        A COMPAS frame.
    scale: float, optional
        Scale factor that controls the length of the axes.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    Attributes
    ----------
    scale : float
        Scale factor that controls the length of the axes.
        Default is ``1.0``.
    color_origin : tuple of 3 int between 0 and 255
        Default is ``(0, 0, 0)``.
    color_xaxis : tuple of 3 int between 0 and 255
        Default is ``(255, 0, 0)``.
    color_yaxis : tuple of 3 int between 0 and 255
        Default is ``(0, 255, 0)``.
    color_zaxis : tuple of 3 int between 0 and 255
        Default is ``(0, 0, 255)``.

    """

    def __init__(self, frame, layer=None, scale=1.0, **kwargs):
        super(FrameArtist, self).__init__(primitive=frame, layer=layer, **kwargs)
        self.scale = scale or 1.0
        self.color_origin = (0, 0, 0)
        self.color_xaxis = (255, 0, 0)
        self.color_yaxis = (0, 255, 0)
        self.color_zaxis = (0, 0, 255)

    def draw(self):
        """Draw the frame.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        points = []
        lines = []
        origin = list(self.primitive.point)
        X = list(self.primitive.point + self.primitive.xaxis.scaled(self.scale))
        Y = list(self.primitive.point + self.primitive.yaxis.scaled(self.scale))
        Z = list(self.primitive.point + self.primitive.zaxis.scaled(self.scale))
        points = [{'pos': origin, 'color': self.color_origin}]
        lines = [
            {'start': origin, 'end': X, 'color': self.color_xaxis, 'arrow': 'end'},
            {'start': origin, 'end': Y, 'color': self.color_yaxis, 'arrow': 'end'},
            {'start': origin, 'end': Z, 'color': self.color_zaxis, 'arrow': 'end'}]
        guids = compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)
        return guids


# src/compas_rhino/artists/lineartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class LineArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing lines.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`
        A COMPAS line.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    """

    def __init__(self, line, layer=None, **kwargs):
        super(LineArtist, self).__init__(primitive=line, layer=layer, **kwargs)

    def draw(self, show_points=False):
        """Draw the line.

        Parameters
        ----------
        show_points : bool, optional
            If True, draw the start and end point of the line.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        start = list(self.primitive.start)
        end = list(self.primitive.end)
        guids = []
        if show_points:
            points = [
                {'pos': start, 'color': self.color, 'name': self.primitive.name},
                {'pos': end, 'color': self.color, 'name': self.primitive.name}
            ]
            guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        lines = [{'start': start, 'end': end, 'color': self.color, 'name': self.primitive.name}]
        guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)
        return guids


# src/compas_rhino/artists/meshartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from functools import partial

from compas.utilities import color_to_colordict
from compas.geometry import add_vectors
from compas.geometry import scale_vector
from compas.geometry import centroid_points

import compas_rhino
from compas.artists import MeshArtist
from .artist import RhinoArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=False)


class MeshArtist(RhinoArtist, MeshArtist):
    """Artists for drawing mesh data structures.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A COMPAS mesh.
    layer : str, optional
        The name of the layer that will contain the mesh.
    vertices : list[int], optional
        Selection of vertices to draw.
    edges : list[tuple[int, int]], optional
        Selection of edges to draw.
    faces : list[int], optional
        Selection of faces to draw.
    vertexcolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
        Color of the vertices.
        Default color is :attr:`MeshArtists.default_vertexcolor`.
    edgecolor : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
        Color of the edges.
        Default color is :attr:`MeshArtists.default_edgecolor`.
    facecolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
        Color of the faces.
        Default color is :attr:`MeshArtists.default_facecolor`.
    show_vertices : bool, optional
        If True, draw the vertices.
    show_edges : bool, optional
        If True, draw the edges.
    show_faces : bool, optional
        If True, draw the faces.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`MeshArtist`.

    Class Attributes
    ----------------
    default_color : tuple[int, int, int]
        Default base color of the mesh.
    default_vertexcolor : tuple[int, int, int]
        Default color for vertices.
    default_edgecolor : tuple[int, int, int]
        Default color for edges.
    default_facecolor : tuple[int, int, int]
        Default color for faces.

    """

    default_color = (0, 0, 0)
    default_vertexcolor = (255, 255, 255)
    default_edgecolor = (0, 0, 0)
    default_facecolor = (200, 200, 200)

    def __init__(self,
                 mesh,
                 layer=None,
                 vertices=None,
                 edges=None,
                 faces=None,
                 vertexcolor=None,
                 edgecolor=None,
                 facecolor=None,
                 show_mesh=False,
                 show_vertices=True,
                 show_edges=True,
                 show_faces=True,
                 **kwargs):

        super(MeshArtist, self).__init__(mesh=mesh, layer=layer, **kwargs)

        self.vertices = vertices
        self.edges = edges
        self.faces = faces
        self.vertex_color = vertexcolor
        self.edge_color = edgecolor
        self.face_color = facecolor
        self.show_mesh = show_mesh
        self.show_vertices = show_vertices
        self.show_edges = show_edges
        self.show_faces = show_faces

    # ==========================================================================
    # clear
    # ==========================================================================

    def clear(self):
        """Delete all objects drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_mesh(self):
        """Delete the mesh drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.mesh".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_vertices(self):
        """Delete all vertices drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_edges(self):
        """Delete all edges drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.edge.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_faces(self):
        """Delete all faces drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.face.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_vertexnormals(self):
        """Delete all vertex normals drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.vertexnormal.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_facenormals(self):
        """Delete all face normals drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.facenormal.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_vertexlabels(self):
        """Delete all vertex labels drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.vertexlabel.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_edgelabels(self):
        """Delete all edge labels drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_facelabels(self):
        """Delete all face labels drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.facelabel.*".format(self.mesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    # ==========================================================================
    # draw
    # ==========================================================================

    def draw(self, vertices=None, edges=None, faces=None, vertexcolor=None, edgecolor=None, facecolor=None, join_faces=False):
        """Draw the mesh using the chosen visualization settings.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertices to draw.
            Default is None, in which case all vertices are drawn.
        edges : list[tuple[int, int]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        faces : list[int], optional
            A selection of faces to draw.
            The default is None, in which case all faces are drawn.
        vertexcolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the vertices.
            The default color is the value of :attr:`MeshArtist.default_vertexcolor`.
        edgecolor : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color of the edges.
            The default color is the value of :attr:`MeshArtist.default_edgecolor`.
        facecolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the faces.
            The default color is the value of :attr:`MeshArtist.default_facecolor`.
        join_faces : bool, optional
            If True, join the faces into a single mesh.

        Returns
        -------
        None

        """
        self.clear()
        if self.show_mesh:
            self.draw_mesh()
        if self.show_vertices:
            self.draw_vertices(vertices=vertices, color=vertexcolor)
        if self.show_edges:
            self.draw_edges(edges=edges, color=edgecolor)
        if self.show_faces:
            self.draw_faces(faces=faces, color=facecolor, join_faces=join_faces)

    def draw_mesh(self, color=None, disjoint=False):
        """Draw the mesh as a consolidated RhinoMesh.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The color of the mesh.
            Default is the value of :attr:`MeshArtist.default_color`.
        disjoint : bool, optional
            If True, draw the faces of the mesh with disjoint vertices.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        Notes
        -----
        The mesh should be a valid Rhino Mesh object, which means it should have only triangular or quadrilateral faces.
        Faces with more than 4 vertices will be triangulated on-the-fly.

        """
        color = color or self.default_color
        vertex_index = self.mesh.vertex_index()
        vertex_xyz = self.vertex_xyz
        vertices = [vertex_xyz[vertex] for vertex in self.mesh.vertices()]
        faces = [[vertex_index[vertex] for vertex in self.mesh.face_vertices(face)] for face in self.mesh.faces()]
        layer = self.layer
        name = "{}.mesh".format(self.mesh.name)
        guid = compas_rhino.draw_mesh(vertices, faces, layer=layer, name=name, color=color, disjoint=disjoint)
        return [guid]

    def draw_vertices(self, vertices=None, color=None):
        """Draw a selection of vertices.

        Parameters
        ----------
        vertices : list[int], optional
            A selection of vertices to draw.
            Default is None, in which case all vertices are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the vertices.
            The default is the value of :attr:`MeshArtist.default_vertexcolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.vertex_color = color
        vertices = vertices or self.vertices
        vertex_xyz = self.vertex_xyz
        points = []
        for vertex in vertices:
            points.append({
                'pos': vertex_xyz[vertex],
                'name': "{}.vertex.{}".format(self.mesh.name, vertex),
                'color': self.vertex_color.get(vertex, self.default_vertexcolor)
            })
        guids = compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        return guids

    def draw_edges(self, edges=None, color=None):
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A selection of edges to draw.
            The default is None, in which case all edges are drawn.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color of the edges.
            The default color is the value of :attr:`MeshArtist.default_edgecolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.edge_color = color
        edges = edges or self.edges
        vertex_xyz = self.vertex_xyz
        lines = []
        for edge in edges:
            lines.append({
                'start': vertex_xyz[edge[0]],
                'end': vertex_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': "{}.edge.{}-{}".format(self.mesh.name, *edge)
            })
        guids = compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)
        return guids

    def draw_faces(self, faces=None, color=None, join_faces=False):
        """Draw a selection of faces.

        Parameters
        ----------
        faces : list[int], optional
            A selection of faces to draw.
            The default is None, in which case all faces are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the faces.
            The default color is the value of :attr:`MeshArtist.default_facecolor`.
        join_faces : bool, optional
            If True, join the faces into a single mesh.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.face_color = color
        faces = faces or self.faces
        vertex_xyz = self.vertex_xyz
        facets = []
        for face in faces:
            facets.append({
                'points': [vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)],
                'name': "{}.face.{}".format(self.mesh.name, face),
                'color': self.face_color.get(face, self.default_facecolor)
            })
        guids = compas_rhino.draw_faces(facets, layer=self.layer, clear=False, redraw=False)
        if join_faces:
            guid = compas_rhino.rs.JoinMeshes(guids, delete_input=True)
            compas_rhino.rs.ObjectLayer(guid, self.layer)
            compas_rhino.rs.ObjectName(guid, '{}.mesh'.format(self.mesh.name))
            compas_rhino.rs.ObjectColor(guid, color)
            guids = [guid]
        return guids

    # ==========================================================================
    # draw normals
    # ==========================================================================

    def draw_vertexnormals(self, vertices=None, color=(0, 255, 0), scale=1.0):
        """Draw the normals at the vertices of the mesh.

        Parameters
        ----------
        vertices : list[int], optional
            A selection of vertex normals to draw.
            Default is to draw all vertex normals.
        color : tuple[int, int, int], optional
            The color specification of the normal vectors.
        scale : float, optional
            Scale factor for the vertex normals.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        vertex_xyz = self.vertex_xyz
        vertices = vertices or self.vertices
        lines = []
        for vertex in vertices:
            a = vertex_xyz[vertex]
            n = self.mesh.vertex_normal(vertex)
            b = add_vectors(a, scale_vector(n, scale))
            lines.append({
                'start': a,
                'end': b,
                'color': color,
                'name': "{}.vertexnormal.{}".format(self.mesh.name, vertex),
                'arrow': 'end'
            })
        return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)

    def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0):
        """Draw the normals of the faces.

        Parameters
        ----------
        faces : list[int], optional
            A selection of face normals to draw.
            Default is to draw all face normals.
        color : tuple[int, int, int], optional
            The color specification of the normal vectors.
        scale : float, optional
            Scale factor for the face normals.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        vertex_xyz = self.vertex_xyz
        faces = faces or self.faces
        lines = []
        for face in faces:
            a = centroid_points([vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)])
            n = self.mesh.face_normal(face)
            b = add_vectors(a, scale_vector(n, scale))
            lines.append({
                'start': a,
                'end': b,
                'name': "{}.facenormal.{}".format(self.mesh.name, face),
                'color': color,
                'arrow': 'end'
            })
        return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)

    # ==========================================================================
    # draw labels
    # ==========================================================================

    def draw_vertexlabels(self, text=None, color=None):
        """Draw labels for a selection vertices.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of vertex labels as vertex-text pairs.
            The default value is None, in which case every vertex will be labelled with its identifier.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color specification of the labels.
            The default color is the same as the default vertex color.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if not text or text == 'key':
            vertex_text = {vertex: str(vertex) for vertex in self.vertices}
        elif text == 'index':
            vertex_text = {vertex: str(index) for index, vertex in enumerate(self.vertices)}
        elif isinstance(text, dict):
            vertex_text = text
        else:
            raise NotImplementedError
        vertex_xyz = self.vertex_xyz
        vertex_color = colordict(color, vertex_text.keys(), default=self.default_vertexcolor)
        labels = []
        for vertex in vertex_text:
            labels.append({
                'pos': vertex_xyz[vertex],
                'name': "{}.vertexlabel.{}".format(self.mesh.name, vertex),
                'color': vertex_color[vertex],
                'text': vertex_text[vertex]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)

    def draw_edgelabels(self, text=None, color=None):
        """Draw labels for a selection of edges.

        Parameters
        ----------
        text : dict[tuple[int, int], str], optional
            A dictionary of edge labels as edge-text pairs.
            The default value is None, in which case every edge will be labelled with its identifier.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color specification of the labels.
            The default color is the same as the default edge color.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if text is None:
            edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.edges}
        elif isinstance(text, dict):
            edge_text = text
        else:
            raise NotImplementedError
        vertex_xyz = self.vertex_xyz
        edge_color = colordict(color, edge_text.keys(), default=self.default_edgecolor)
        labels = []
        for edge in edge_text:
            labels.append({
                'pos': centroid_points([vertex_xyz[edge[0]], vertex_xyz[edge[1]]]),
                'name': "{}.edgelabel.{}-{}".format(self.mesh.name, *edge),
                'color': edge_color[edge],
                'text': edge_text[edge]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)

    def draw_facelabels(self, text=None, color=None):
        """Draw labels for a selection of faces.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of face labels as face-text pairs.
            The default value is None, in which case every face will be labelled with its key.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color specification of the labels.
            The default color is the same as the default face color.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if not text or text == 'key':
            face_text = {face: str(face) for face in self.faces}
        elif text == 'index':
            face_text = {face: str(index) for index, face in enumerate(self.faces)}
        elif isinstance(text, dict):
            face_text = text
        else:
            raise NotImplementedError
        vertex_xyz = self.vertex_xyz
        face_color = colordict(color, face_text.keys(), default=self.default_facecolor)
        labels = []
        for face in face_text:
            labels.append({
                'pos': centroid_points([vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]),
                'name': "{}.facelabel.{}".format(self.mesh.name, face),
                'color': face_color[face],
                'text': face_text[face]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)


# src/compas_rhino/artists/networkartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from functools import partial
import compas_rhino

from compas.geometry import centroid_points
from compas.utilities import color_to_colordict

from compas.artists import NetworkArtist
from .artist import RhinoArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=False)


class NetworkArtist(RhinoArtist, NetworkArtist):
    """Artist for drawing network data structures.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A COMPAS network.
    layer : str, optional
        The parent layer of the network.
    nodes : list[int], optional
        A list of node identifiers.
        Default is None, in which case all nodes are drawn.
    edges : list[tuple[int, int]], optional
        A list of edge identifiers.
        The default is None, in which case all edges are drawn.
    nodecolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
        The color of the nodes.
    edgecolor : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
        The color of the edges.
    show_nodes : bool, optional
        If True, draw the nodes of the network.
    show_edges : bool, optional
        If True, draw the edges of the network.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`NetworkArtist`.

    """

    def __init__(self,
                 network,
                 layer=None,
                 nodes=None,
                 edges=None,
                 nodecolor=None,
                 edgecolor=None,
                 show_nodes=True,
                 show_edges=True,
                 **kwargs):

        super(NetworkArtist, self).__init__(network=network, layer=layer, **kwargs)

        self.nodes = nodes
        self.edges = edges
        self.node_color = nodecolor
        self.edge_color = edgecolor
        self.show_nodes = show_nodes
        self.show_edges = show_edges

    # ==========================================================================
    # clear
    # ==========================================================================

    def clear(self):
        """Delete all objects drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.*".format(self.network.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_nodes(self):
        """Delete all nodes drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.network.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_edges(self):
        """Delete all edges drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.edge.*".format(self.network.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_nodelabels(self):
        """Delete all node labels drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.nodexlabel.*".format(self.network.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_edgelabels(self):
        """Delete all edge labels drawn by this artist.

        Returns
        -------
        None

        """
        guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.network.name))
        compas_rhino.delete_objects(guids, purge=True)

    # ==========================================================================
    # draw
    # ==========================================================================

    def draw(self, nodes=None, edges=None, nodecolor=None, edgecolor=None):
        """Draw the network using the chosen visualisation settings.

        Parameters
        ----------
        nodes : list[int], optional
            A list of nodes to draw.
            Default is None, in which case all nodes are drawn.
        edges : list[tuple[int, int]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        nodecolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the nodes.
            The default color is :attr:`NetworkArtist.default_nodecolor`.
        edgecolor : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color of the edges.
            The default color is :attr:`NetworkArtist.default_edgecolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.clear()
        guids = self.draw_nodes(nodes=nodes, color=nodecolor)
        guids += self.draw_edges(edges=edges, color=edgecolor)
        return guids

    def draw_nodes(self, nodes=None, color=None):
        """Draw a selection of nodes.

        Parameters
        ----------
        nodes : list[int], optional
            A list of nodes to draw.
            Default is None, in which case all nodes are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            Color of the nodes.
            The default color is :attr:`NetworkArtist.default_nodecolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.node_color = color
        node_xyz = self.node_xyz
        nodes = nodes or self.nodes
        points = []
        for node in nodes:
            points.append({
                'pos': node_xyz[node],
                'name': "{}.node.{}".format(self.network.name, node),
                'color': self.node_color.get(node, self.default_nodecolor)
            })
        return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)

    def draw_edges(self, edges=None, color=None):
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            Color of the edges.
            The default color is :attr:`NetworkArtist.default_edgecolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.edge_color = color
        node_xyz = self.node_xyz
        edges = edges or self.edges
        lines = []
        for edge in edges:
            lines.append({
                'start': node_xyz[edge[0]],
                'end': node_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': "{}.edge.{}-{}".format(self.network.name, *edge)
            })
        return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)

    # ==========================================================================
    # draw labels
    # ==========================================================================

    def draw_nodelabels(self, text=None, color=None):
        """Draw labels for a selection nodes.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of node labels as node-text pairs.
            The default value is None, in which case every node will be labelled with its key.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            Color of the labels.
            The default color is the same as the default color of the nodes.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if not text or text == 'key':
            node_text = {node: str(node) for node in self.nodes}
        elif text == 'index':
            node_text = {node: str(index) for index, node in enumerate(self.nodes)}
        elif isinstance(text, dict):
            node_text = text
        else:
            raise NotImplementedError
        node_xyz = self.node_xyz
        node_color = colordict(color, node_text.keys(), default=self.default_nodecolor)
        labels = []
        for node in node_text:
            labels.append({
                'pos': node_xyz[node],
                'name': "{}.nodelabel.{}".format(self.network.name, node),
                'color': node_color[node],
                'text': node_text[node]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)

    def draw_edgelabels(self, text=None, color=None):
        """Draw labels for a selection of edges.

        Parameters
        ----------
        text : dict[tuple[int, int], str], optional
            A dictionary of edgelabels as edge-text pairs.
            The default value is None, in which case every edge will be labelled with its key.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            Color of the labels.
            The default color is the same as the default color of the edges.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if text is None:
            edge_text = {edge: "{}-{}".format(*edge) for edge in self.edges}
        elif isinstance(text, dict):
            edge_text = text
        else:
            raise NotImplementedError
        node_xyz = self.node_xyz
        edge_color = colordict(color, edge_text.keys(), default=self.default_edgecolor)
        labels = []
        for edge in edge_text:
            labels.append({
                'pos': centroid_points([node_xyz[edge[0]], node_xyz[edge[1]]]),
                'name': "{}.edgelabel.{}-{}".format(self.network.name, *edge),
                'color': edge_color[edge],
                'text': edge_text[edge]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)


# src/compas_rhino/artists/planeartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class PlaneArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing planes.

    Parameters
    ----------
    plane : :class:`compas.geometry.Plane`
        A COMPAS plane.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    """

    def __init__(self, plane, layer=None, **kwargs):
        super(PlaneArtist, self).__init__(primitive=plane, layer=layer, **kwargs)

    def draw(self):
        """Draw the plane.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        raise NotImplementedError


# src/compas_rhino/artists/pointartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class PointArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing points.

    Parameters
    ----------
    point : :class:`compas.geometry.Point`
        A COMPAS point.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    """

    def __init__(self, point, layer=None, **kwargs):
        super(PointArtist, self).__init__(primitive=point, layer=layer, **kwargs)

    def draw(self):
        """Draw the point.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        points = [{'pos': list(self.primitive), 'color': self.color, 'name': self.primitive.name}]
        guids = compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        return guids


# src/compas_rhino/artists/polygonartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class PolygonArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing polygons.

    Parameters
    ----------
    polygon : :class:`compas.geometry.Polygon`
        A COMPAS polygon.
    layer : str, optional
        The name of the layer that will contain the mesh.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    """

    def __init__(self, polygon, layer=None, **kwargs):
        super(PolygonArtist, self).__init__(primitive=polygon, layer=layer, **kwargs)

    def draw(self, show_points=False, show_edges=False, show_face=True):
        """Draw the polygon.

        Parameters
        ----------
        show_points : bool, optional
            If True, draw the corner points of the polygon.
        show_edges : bool, optional
            If True, draw the boundary edges of the polygon.
        show_face : bool, optional
            If True, draw the face of the polygon.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        _points = map(list, self.primitive.points)
        guids = []
        if show_points:
            points = [{'pos': point, 'color': self.color, 'name': self.primitive.name} for point in _points]
            guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        if show_edges:
            lines = [{'start': list(a), 'end': list(b), 'color': self.color, 'name': self.primitive.name} for a, b in self.primitive.lines]
            guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)
        if show_face:
            polygons = [{'points': _points, 'color': self.color, 'name': self.primitive.name}]
            guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False)
        return guids


# src/compas_rhino/artists/polyhedronartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import ShapeArtist
from .artist import RhinoArtist


class PolyhedronArtist(RhinoArtist, ShapeArtist):
    """Artist for drawing polyhedron shapes.

    Parameters
    ----------
    polyhedron : :class:`compas.geometry.Polyhedron`
        A COMPAS polyhedron.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`.

    """

    def __init__(self, polyhedron, layer=None, **kwargs):
        super(PolyhedronArtist, self).__init__(shape=polyhedron, layer=layer, **kwargs)

    def draw(self, color=None):
        """Draw the polyhedron associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The RGB color of the polyhedron.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the objects created in Rhino.

        """
        color = color or self.color
        vertices = [list(vertex) for vertex in self.shape.vertices]
        faces = self.shape.faces
        guid = compas_rhino.draw_mesh(vertices,
                                      faces,
                                      layer=self.layer,
                                      name=self.shape.name,
                                      color=color,
                                      disjoint=True)
        return [guid]


# src/compas_rhino/artists/polylineartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class PolylineArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing polylines.

    Parameters
    ----------
    polyline : :class:`compas.geometry.Polyline`
        A COMPAS polyline.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    """

    def __init__(self, polyline, layer=None, **kwargs):
        super(PolylineArtist, self).__init__(primitive=polyline, layer=layer, **kwargs)

    def draw(self, show_points=False):
        """Draw the polyline.

        Parameters
        ----------
        show_points : bool, optional
            If True, draw the points of the polyline.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        _points = map(list, self.primitive.points)
        guids = []
        if show_points:
            points = [{'pos': point, 'color': self.color, 'name': self.primitive.name} for point in _points]
            guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        polylines = [{'points': _points, 'color': self.color, 'name': self.primitive.name}]
        guids = compas_rhino.draw_polylines(polylines, layer=self.layer, clear=False, redraw=False)
        return guids


# src/compas_rhino/artists/robotmodelartist.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import time

import Rhino.Geometry
import scriptcontext as sc
from System.Drawing import Color
from Rhino.DocObjects.ObjectColorSource import ColorFromObject
from Rhino.DocObjects.ObjectColorSource import ColorFromLayer
from Rhino.DocObjects.ObjectMaterialSource import MaterialFromObject

from compas.geometry import centroid_polygon
from compas.utilities import pairwise
from compas.artists import RobotModelArtist

import compas_rhino
from compas_rhino.artists import RhinoArtist
from compas_rhino.geometry.transformations import xform_from_transformation


class RobotModelArtist(RhinoArtist, RobotModelArtist):
    """Artist for drawing robot models.

    Parameters
    ----------
    model : :class:`compas.robots.RobotModel`
        Robot model.
    layer : str, optional
        The name of the layer that will contain the robot meshes.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`RobotModelArtist`.

    """

    def __init__(self, model, layer=None, **kwargs):
        super(RobotModelArtist, self).__init__(model=model, layer=layer, **kwargs)

    def transform(self, native_mesh, transformation):
        T = xform_from_transformation(transformation)
        native_mesh.Transform(T)

    def create_geometry(self, geometry, name=None, color=None):
        """Create a Rhino mesh corresponding to the geometry of the model.

        Parameters
        ----------
        geometry : :class:`compas.datastructures.Mesh`
            A COMPAS mesh data structure.
        name : str, optional
            Name of the mesh object.
        color : tuple[int, int, int], optional
            Color of the mesh object.

        Returns
        -------
        :rhino:`Rhino.Geometry.Mesh`

        """
        # Imported colors take priority over a the parameter color
        if 'mesh_color.diffuse' in geometry.attributes:
            color = geometry.attributes['mesh_color.diffuse']

        key_index = geometry.key_index()
        vertices = geometry.vertices_attributes('xyz')
        faces = [[key_index[key] for key in geometry.face_vertices(fkey)] for fkey in geometry.faces()]
        new_faces = []
        for face in faces:
            f = len(face)
            if f == 3:
                new_faces.append(face + face[-1:])
            elif f == 4:
                new_faces.append(face)
            elif f > 4:
                centroid = len(vertices)
                vertices.append(centroid_polygon([vertices[index] for index in face]))
                for a, b in pairwise(face + face[0:1]):
                    new_faces.append([centroid, a, b, b])
            else:
                continue

        mesh = Rhino.Geometry.Mesh()

        if name:
            mesh.UserDictionary.Set('MeshName', name)

        if color:
            r, g, b, a = color
            mesh.UserDictionary.Set('MeshColor.R', r)
            mesh.UserDictionary.Set('MeshColor.G', g)
            mesh.UserDictionary.Set('MeshColor.B', b)
            mesh.UserDictionary.Set('MeshColor.A', a)

        for v in vertices:
            mesh.Vertices.Add(*v)
        for face in new_faces:
            mesh.Faces.AddFace(*face)

        mesh.Normals.ComputeNormals()
        mesh.Compact()

        # Try to fix invalid meshes
        if not mesh.IsValid:
            mesh.FillHoles()

        return mesh

    def _enter_layer(self):
        self._previous_layer = None

        if self.layer:
            if not compas_rhino.rs.IsLayer(self.layer):
                compas_rhino.create_layers_from_path(self.layer)
            self._previous_layer = compas_rhino.rs.CurrentLayer(self.layer)

        compas_rhino.rs.EnableRedraw(False)

    def _exit_layer(self):
        if self.layer and self._previous_layer:
            compas_rhino.rs.CurrentLayer(self._previous_layer)

        self.redraw()

    def draw_collision(self):
        """Draw all the collision geometries of the robot model.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        collisions = super(RobotModelArtist, self).draw_collision()
        self._enter_layer()

        new_guids = []
        for mesh in collisions:
            guids = self._add_mesh_to_doc(mesh)
            new_guids.extend(guids)

        self._exit_layer()
        return new_guids

    def draw_visual(self):
        """Draw all the visual geometries of the robot model.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        visuals = super(RobotModelArtist, self).draw_visual()
        self._enter_layer()

        new_guids = []
        for mesh in visuals:
            guids = self._add_mesh_to_doc(mesh)
            new_guids.extend(guids)

        self._exit_layer()
        return new_guids

    def draw_attached_meshes(self):
        """Draw all the geometries attached to the robot model.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        acms = super(RobotModelArtist, self).draw_attached_meshes()
        self._enter_layer()

        new_guids = []
        for mesh in acms:
            guids = self._add_mesh_to_doc(mesh)
            new_guids.extend(guids)

        self._exit_layer()
        return new_guids

    def draw(self):
        """Draw the geometry of the model.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        return self.draw_visual()

    def redraw(self, timeout=None):
        """Redraw the Rhino view.

        Parameters
        ----------
        timeout : float, optional
            The amount of time the artist waits before updating the Rhino view.
            The time should be specified in seconds.

        Returns
        -------
        None

        """
        if timeout:
            time.sleep(timeout)

        compas_rhino.rs.EnableRedraw(True)
        compas_rhino.rs.Redraw()

    def clear_layer(self):
        """Clear the main layer of the artist.

        Returns
        -------
        None

        """
        if self.layer:
            compas_rhino.clear_layer(self.layer)
        else:
            compas_rhino.clear_current_layer()

    def _add_mesh_to_doc(self, mesh):
        guid = sc.doc.Objects.AddMesh(mesh)

        color = None
        if 'MeshColor.R' in mesh.UserDictionary:
            color = [mesh.UserDictionary['MeshColor.R'],
                     mesh.UserDictionary['MeshColor.G'],
                     mesh.UserDictionary['MeshColor.B'],
                     mesh.UserDictionary['MeshColor.A']]
        name = mesh.UserDictionary['MeshName'] if 'MeshName' in mesh.UserDictionary else None

        obj = sc.doc.Objects.Find(guid)

        if obj:
            attr = obj.Attributes
            if color:
                r, g, b, a = [i * 255 for i in color]
                attr.ObjectColor = Color.FromArgb(a, r, g, b)
                attr.ColorSource = ColorFromObject

                material_name = 'robotmodelartist.{:.2f}_{:.2f}_{:.2f}_{:.2f}'.format(r, g, b, a)
                material_index = sc.doc.Materials.Find(material_name, True)

                # Material does not exist, create it
                if material_index == -1:
                    material_index = sc.doc.Materials.Add()
                    material = sc.doc.Materials[material_index]
                    material.Name = material_name
                    material.DiffuseColor = attr.ObjectColor
                    material.CommitChanges()

                attr.MaterialIndex = material_index
                attr.MaterialSource = MaterialFromObject
            else:
                attr.ColorSource = ColorFromLayer

            if name:
                attr.Name = name

            obj.CommitChanges()
        return [guid]


# src/compas_rhino/artists/sphereartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import ShapeArtist
from .artist import RhinoArtist


class SphereArtist(RhinoArtist, ShapeArtist):
    """Artist for drawing sphere shapes.

    Parameters
    ----------
    sphere : :class:`compas.geometry.Sphere`
        A COMPAS sphere.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`.

    """

    def __init__(self, sphere, layer=None, **kwargs):
        super(SphereArtist, self).__init__(shape=sphere, layer=layer, **kwargs)

    def draw(self, color=None, u=None, v=None):
        """Draw the sphere associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The color of the sphere.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`SphereArtist.u`.
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`SphereArtist.v`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the objects created in Rhino.

        """
        color = color or self.color
        u = u or self.u
        v = v or self.v
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        vertices = [list(vertex) for vertex in vertices]
        guid = compas_rhino.draw_mesh(vertices,
                                      faces,
                                      layer=self.layer,
                                      name=self.shape.name,
                                      color=color,
                                      disjoint=True)
        return [guid]


# src/compas_rhino/artists/torusartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino
from compas.artists import ShapeArtist
from .artist import RhinoArtist


class TorusArtist(RhinoArtist, ShapeArtist):
    """Artist for drawing torus shapes.

    Parameters
    ----------
    torus : :class:`compas.geometry.Torus`
        A COMPAS torus.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`.

    """

    def __init__(self, torus, layer=None, **kwargs):
        super(TorusArtist, self).__init__(shape=torus, layer=layer, **kwargs)

    def draw(self, color=None, u=None, v=None):
        """Draw the torus associated with the artist.

        Parameters
        ----------
        color : tuple[int, int, int], optional
            The color of the torus.
        u : int, optional
            Number of faces in the "u" direction.
            Default is :attr:`TorusArtist.u`.
        v : int, optional
            Number of faces in the "v" direction.
            Default is :attr:`TorusArtist.v`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the objects created in Rhino.

        """
        color = color or self.color
        u = u or self.u
        v = v or self.v
        vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v)
        vertices = [list(vertex) for vertex in vertices]
        guid = compas_rhino.draw_mesh(vertices,
                                      faces,
                                      layer=self.layer,
                                      name=self.shape.name,
                                      color=color,
                                      disjoint=True)
        return [guid]


# src/compas_rhino/artists/vectorartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.geometry import Point
import compas_rhino
from compas.artists import PrimitiveArtist
from .artist import RhinoArtist


class VectorArtist(RhinoArtist, PrimitiveArtist):
    """Artist for drawing vectors.

    Parameters
    ----------
    vector : :class:`compas.geometry.Vector`
        A COMPAS vector.
    layer : str, optional
        The layer that should contain the drawing.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`.

    """

    def __init__(self, vector, layer=None, **kwargs):
        super(VectorArtist, self).__init__(primitive=vector, layer=layer, **kwargs)

    def draw(self, point=None, show_point=False):
        """Draw the vector.

        Parameters
        ----------
        point : [float, float, float] or :class:`compas.geometry.Point`, optional
            Point of application of the vector.
            Default is ``Point(0, 0, 0)``.
        show_point : bool, optional
            If True, draw the base point of the vector.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if not point:
            point = [0, 0, 0]
        start = Point(*point)
        end = start + self.primitive
        start = list(start)
        end = list(end)
        guids = []
        if show_point:
            points = [{'pos': start, 'color': self.color, 'name': self.primitive.name}]
            guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)
        lines = [{'start': start, 'end': end, 'arrow': 'end', 'color': self.color, 'name': self.primitive.name}]
        guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)
        return guids


# src/compas_rhino/artists/volmeshartist.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from functools import partial
import compas_rhino

from compas.utilities import color_to_colordict
from compas.geometry import centroid_points

from compas.artists import VolMeshArtist
from .artist import RhinoArtist

colordict = partial(color_to_colordict, colorformat='rgb', normalize=False)


class VolMeshArtist(RhinoArtist, VolMeshArtist):
    """Artist for drawing volmesh data structures.

    Parameters
    ----------
    volmesh : :class:`compas.datastructures.VolMesh`
        A COMPAS volmesh.
    layer : str, optional
        The name of the layer that will contain the volmesh.
    **kwargs : dict, optional
        Additional keyword arguments.
        For more info, see :class:`RhinoArtist` and :class:`VolMeshArtist`.

    """

    def __init__(self, volmesh, layer=None, **kwargs):
        super(VolMeshArtist, self).__init__(volmesh=volmesh, layer=layer, **kwargs)

    # ==========================================================================
    # clear
    # ==========================================================================

    def clear(self):
        guids = compas_rhino.get_objects(name="{}.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_vertices(self):
        guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_edges(self):
        guids = compas_rhino.get_objects(name="{}.edge.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_faces(self):
        guids = compas_rhino.get_objects(name="{}.face.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_cells(self):
        guids = compas_rhino.get_objects(name="{}.cell.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_vertexlabels(self):
        guids = compas_rhino.get_objects(name="{}.vertexlabel.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_edgelabels(self):
        guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    def clear_facelabels(self):
        guids = compas_rhino.get_objects(name="{}.facelabel.*".format(self.volmesh.name))
        compas_rhino.delete_objects(guids, purge=True)

    # ==========================================================================
    # draw
    # ==========================================================================

    def draw(self, vertices=None, edges=None, faces=None, cells=None, vertexcolor=None, edgecolor=None, facecolor=None, cellcolor=None):
        """Draw the network using the chosen visualisation settings.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertices to draw.
            Default is None, in which case all vertices are drawn.
        edges : list[tuple[int, int]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        faces : list[int], optional
            A selection of faces to draw.
            The default is None, in which case all faces are drawn.
        cells : list[int], optional
            A selection of cells to draw.
            The default is None, in which case all cells are drawn.
        vertexcolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the vertices.
            The default color is the value of :attr:`VolMeshArtist.default_vertexcolor`.
        edgecolor : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color of the edges.
            The default color is the value of :attr:`VolMeshArtist.default_edgecolor`.
        facecolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the faces.
            The default color is the value of :attr:`VolMeshArtist.default_facecolor`.
        cellcolor : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the cells.
            The default color is the value of :attr:`VolMeshArtist.default_cellcolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        guids = self.draw_vertices(vertices=vertices, color=vertexcolor)
        guids += self.draw_edges(edges=edges, color=edgecolor)
        guids += self.draw_faces(faces=faces, color=facecolor)
        guids += self.draw_cells(cells=cells, color=cellcolor)
        return guids

    def draw_vertices(self, vertices=None, color=None):
        """Draw a selection of vertices.

        Parameters
        ----------
        vertices : list[int], optional
            A list of vertices to draw.
            Default is None, in which case all vertices are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the vertices.
            The default color of the vertices is :attr:`VolMeshArtist.default_vertexcolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.vertex_color = color
        vertices = vertices or list(self.volmesh.vertices())
        vertex_xyz = self.vertex_xyz
        points = []
        for vertex in vertices:
            points.append({
                'pos': vertex_xyz[vertex],
                'name': "{}.vertex.{}".format(self.volmesh.name, vertex),
                'color': self.vertex_color.get(vertex, self.default_vertexcolor)
            })
        return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False)

    def draw_edges(self, edges=None, color=None):
        """Draw a selection of edges.

        Parameters
        ----------
        edges : list[tuple[int, int]], optional
            A list of edges to draw.
            The default is None, in which case all edges are drawn.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            The color of the edges.
            The default color is :attr:`VolMeshArtist.default_edgecolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.edge_color = color
        edges = edges or list(self.volmesh.edges())
        vertex_xyz = self.vertex_xyz
        lines = []
        for edge in edges:
            lines.append({
                'start': vertex_xyz[edge[0]],
                'end': vertex_xyz[edge[1]],
                'color': self.edge_color.get(edge, self.default_edgecolor),
                'name': "{}.edge.{}-{}".format(self.volmesh.name, *edge)
            })
        return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False)

    def draw_faces(self, faces=None, color=None):
        """Draw a selection of faces.

        Parameters
        ----------
        faces : list[int], optional
            A list of faces to draw.
            The default is None, in which case all faces are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the faces.
            The default color is :attr:`VolMeshArtist.default_facecolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        self.face_color = color
        faces = faces or list(self.volmesh.faces())
        vertex_xyz = self.vertex_xyz
        facets = []
        for face in faces:
            facets.append({
                'points': [vertex_xyz[vertex] for vertex in self.volmesh.halfface_vertices(face)],
                'name': "{}.face.{}".format(self.volmesh.name, face),
                'color': self.face_color.get(face, self.default_facecolor)
            })
        return compas_rhino.draw_faces(facets, layer=self.layer, clear=False, redraw=False)

    def draw_cells(self, cells=None, color=None):
        """Draw a selection of cells.

        Parameters
        ----------
        cells : list[int], optional
            A list of cells to draw.
            The default is None, in which case all cells are drawn.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            The color of the cells.
            The default color is :attr:`VolMeshArtist.default_cellcolor`.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.
            Every cell is drawn as an individual mesh.

        """
        self.cell_color = color
        cells = cells or list(self.volmesh.cells())
        vertex_xyz = self.vertex_xyz
        meshes = []
        for cell in cells:
            cell_faces = []
            for fkey in self.volmesh.cell_faces(cell):
                cell_faces.append({
                    'points': [vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(fkey)],
                    'name': "{}.cell.{}.face.{}".format(self.volmesh.name, cell, fkey),
                    'color': self.cell_color.get(cell, self.default_cellcolor)
                })
            guids = compas_rhino.draw_faces(cell_faces, layer=self.layer, clear=False, redraw=False)
            guid = compas_rhino.rs.JoinMeshes(guids, delete_input=True)
            compas_rhino.rs.ObjectLayer(guid, self.layer)
            compas_rhino.rs.ObjectName(guid, '{}.cell.{}'.format(self.volmesh.name, cell))
            compas_rhino.rs.ObjectColor(guid, self.cell_color.get(cell, self.default_cellcolor))
            meshes.append(guid)
        return meshes

    # ==========================================================================
    # draw labels
    # ==========================================================================

    def draw_vertexlabels(self, text=None, color=None):
        """Draw labels for a selection vertices.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of vertex labels as vertex-text pairs.
            The default value is None, in which case every vertex will be labelled with its key.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            Color of the labels.
            The default color is the same as the color of the vertices.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if not text or text == 'key':
            vertex_text = {vertex: str(vertex) for vertex in self.volmesh.vertices()}
        elif text == 'index':
            vertex_text = {vertex: str(index) for index, vertex in enumerate(self.volmesh.vertices())}
        elif isinstance(text, dict):
            vertex_text = text
        else:
            raise NotImplementedError
        vertex_xyz = self.vertex_xyz
        vertex_color = colordict(color, vertex_text.keys(), default=self.color_vertices)
        labels = []
        for vertex in vertex_text:
            labels.append({
                'pos': vertex_xyz[vertex],
                'name': "{}.vertexlabel.{}".format(self.volmesh.name, vertex),
                'color': vertex_color[vertex],
                'text': vertex_text[vertex]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)

    def draw_edgelabels(self, text=None, color=None):
        """Draw labels for a selection of edges.

        Parameters
        ----------
        text : dict[tuple[int, int], str], optional
            A dictionary of edge labels as edge-text pairs.
            The default value is None, in which case every edge will be labelled with its key.
        color : tuple[int, int, int] or dict[tuple[int, int], tuple[int, int, int]], optional
            Color of the labels.
            The default color is tha same as the color of the edges.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if text is None:
            edge_text = {edge: "{}-{}".format(*edge) for edge in self.volmesh.edges()}
        elif isinstance(text, dict):
            edge_text = text
        else:
            raise NotImplementedError
        vertex_xyz = self.vertex_xyz
        edge_color = colordict(color, edge_text.keys(), default=self.color_edges)
        labels = []
        for edge in edge_text:
            labels.append({
                'pos': centroid_points([vertex_xyz[edge[0]], vertex_xyz[edge[1]]]),
                'name': "{}.edgelabel.{}-{}".format(self.volmesh.name, *edge),
                'color': edge_color[edge],
                'text': edge_text[edge]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)

    def draw_facelabels(self, text=None, color=None):
        """Draw labels for a selection of faces.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of face labels as face-text pairs.
            The default value is None, in which case every face will be labelled with its key.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            Color of the labels.
            The default color is the same as the color of the faces.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if not text or text == 'key':
            face_text = {face: str(face) for face in self.volmesh.faces()}
        elif text == 'index':
            face_text = {face: str(index) for index, face in enumerate(self.volmesh.faces())}
        elif isinstance(text, dict):
            face_text = text
        else:
            raise NotImplementedError
        vertex_xyz = self.vertex_xyz
        face_color = colordict(color, face_text.keys(), default=self.color_faces)
        labels = []
        for face in face_text:
            labels.append({
                'pos': centroid_points([vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)]),
                'name': "{}.facelabel.{}".format(self.volmesh.name, face),
                'color': face_color[face],
                'text': face_text[face]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)

    def draw_celllabels(self, text=None, color=None):
        """Draw labels for cells.

        Parameters
        ----------
        text : dict[int, str], optional
            A dictionary of cell labels as cell-text pairs.
            The default value is None, in which case every cell will be labelled with its key.
        color : tuple[int, int, int] or dict[int, tuple[int, int, int]], optional
            Color of the labels.
            The default color is the same as the color of the cells.

        Returns
        -------
        list[System.Guid]
            The GUIDs of the created Rhino objects.

        """
        if not text or text == 'key':
            cell_text = {cell: str(cell) for cell in self.volmesh.cells()}
        elif text == 'index':
            cell_text = {cell: str(index) for index, cell in enumerate(self.volmesh.cells())}
        elif isinstance(text, dict):
            cell_text = text
        else:
            raise NotImplementedError
        vertex_xyz = self.vertex_xyz
        cell_color = colordict(color, cell_text.keys(), default=self.color_cells)
        labels = []
        for cell in cell_text:
            labels.append({
                'pos': centroid_points([vertex_xyz[vertex] for vertex in self.volmesh.cell_vertices(cell)]),
                'name': "{}.facelabel.{}".format(self.volmesh.name, cell),
                'color': cell_color[cell],
                'text': cell_text[cell]
            })
        return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False)


# src/compas_rhino/conduits/__init__.py

"""
********************************************************************************
conduits
********************************************************************************

.. currentmodule:: compas_rhino.conduits

.. rst-class:: lead

Display conduits can be used to visualize iterative processes
with less of a performance penalty than with regular geometry objects.

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    BaseConduit
    FacesConduit
    LabelsConduit
    LinesConduit
    PointsConduit

"""
from __future__ import absolute_import

from .base import BaseConduit

from .faces import FacesConduit
from .labels import LabelsConduit
from .lines import LinesConduit
from .points import PointsConduit

__all__ = [
    'BaseConduit',
    'FacesConduit',
    'LabelsConduit',
    'LinesConduit',
    'PointsConduit'
]


# src/compas_rhino/conduits/base.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import time
from contextlib import contextmanager

import Rhino
import scriptcontext as sc


class BaseConduit(Rhino.Display.DisplayConduit):
    """Base class for conduits.

    Parameters
    ----------
    refreshrate : int, optional
        The number of iterations after which the conduit should be redrawn.

    """

    def __init__(self, refreshrate=1):
        super(BaseConduit, self).__init__()
        self.refreshrate = refreshrate

    @contextmanager
    def enabled(self):
        """Create a context for the conduit with automatic enabling and disabling.

        Yields
        ------
        None

        Notes
        -----
        The conduit is automatically enabled when the context is entered,
        and is guaranteed to be disabled when the context is exited,
        even when an error occurs during the execution of the code in the context.

        Examples
        --------
        .. code-block:: python

            with conduit.enabled():
                for i in range(10):
                    conduit.redraw(k=1)

        """
        self.enable()
        try:
            yield
        except Exception as e:
            print(e)
        finally:
            self.disable()

    def CalculateBoundingBox(self, e):
        """Calculate the model extents that should be included in the visualization.

        Parameters
        ----------
        e : Rhino.DisplayCalculateBoundingBoxEventArgs

        Returns
        -------
        None

        """
        bbox = Rhino.Geometry.BoundingBox(-1000, -1000, -1000, 1000, 1000, 1000)
        e.IncludeBoundingBox(bbox)

    def enable(self):
        """Enable the conduit.

        Returns
        -------
        None

        """
        self.Enabled = True

    def disable(self):
        """Disable the conduit.

        Returns
        -------
        None

        """
        self.Enabled = False

    def redraw(self, k=0, pause=0.0):
        """Redraw the conduit.

        Parameters
        ----------
        k : int, optional
            The current iteration.
            If the current iteration is a multiple of :attr:`BaseConduit.refreshrate`, the conduit will be redrawn.
        pause : float, optional
            Include a pause after redrawing.
            The pause value should be provided in seconds.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If `pause` is not a positive number.

        """
        if pause < 0:
            raise ValueError("The value of pause should be a positive number.")

        if k % self.refreshrate == 0:
            sc.doc.Views.Redraw()
        Rhino.RhinoApp.Wait()
        if pause:
            time.sleep(pause)


# src/compas_rhino/conduits/faces.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from System.Drawing.Color import FromArgb
from Rhino.Geometry import Point3d

from compas.utilities import iterable_like
from compas.utilities import is_sequence_of_iterable

from .base import BaseConduit


class FacesConduit(BaseConduit):
    """A Rhino display conduit for faces.

    Parameters
    ----------
    vertices : list[[float, float, float] or :class:`compas.geometry.Point`]
        The coordinates of the vertices of the faces.
    faces : list[list[int]]
        The faces defined as lists of indices in `vertices`.
    color : tuple[int, int, int] or list[tuple[int, int, int]], optional
        The colors of the faces.
        Default is None, in which case the default color is used for all faces (:attr:`FacesConduit.default_color`).

    Attributes
    ----------
    color : list[System.Drawing.Color]
        The color specification per face.

    Class Attributes
    ----------------
    default_color : System.Drawing.Color
        The default color is ``FromArgb(255, 255, 255)``.

    Examples
    --------
    .. code-block:: python

        from compas.geometry import Polyhedron
        from compas_rhino.conduits import FacesConduit

        polyhedron = Polyhedron.generate(6)
        conduit = FacesConduit(polyhedron.vertices, polyhedron.faces)

        with conduit.enabled():
            conduit.redraw(pause=5.0)

    """

    default_color = FromArgb(255, 255, 255)

    def __init__(self, vertices, faces, color=None, **kwargs):
        super(FacesConduit, self).__init__(**kwargs)
        self._color = None
        self.vertices = vertices or []
        self.faces = faces or []
        self.color = color

    @property
    def color(self):
        return self._color

    @color.setter
    def color(self, color):
        if not color:
            return
        if not is_sequence_of_iterable(color):
            color = [color]
        color = [FromArgb(* c) for c in iterable_like(self.faces, color, self.default_color)]
        self._color = color

    def DrawForeground(self, e):
        """Draw the faces as polygons.

        Parameters
        ----------
        e : Rhino.Display.DrawEventArgs

        Returns
        -------
        None

        """
        for i, face in enumerate(self.faces):
            points = [Point3d(* self.vertices[key]) for key in face]
            if self.color:
                e.Display.DrawPolygon(points, self.color[i], True)
            else:
                e.Display.DrawPolygon(points, self.default_color, True)


# src/compas_rhino/conduits/labels.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from System.Drawing.Color import FromArgb
from Rhino.Geometry import Point3d

from compas.utilities import iterable_like
from compas.utilities import is_sequence_of_iterable
from .base import BaseConduit


class LabelsConduit(BaseConduit):
    """A Rhino display conduit for labels.

    Parameters
    ----------
    labels : list[tuple[[float, float, float] or :class:`compas.geometry.Point`, str]]
        A list of label tuples.
        Each tuple contains a position and text for the label.
    color : list[tuple[tuple[int, int, int], tuple[int, int, int]]], optional
        The colors of the labels.
        Each color is a tuple with a background color and a text color.
        The default background color is :attr:`LabelsConduit.default_color`,
        and the default text color is :attr:`LabelsConduit.default_textcolor`.

    Attributes
    ----------
    color : list[tuple[System.Drawing.Color, System.Drawing.Color]]
        A color specification per label.
    labels : list[tuple[[float, float, float] or :class:`compas.geometry.Point`, str]]
        A list of label tuples.
        Each tuple contains a position and text for the label.

    Class Attributes
    ----------------
    default_color : System.Drawing.Color
        The default background color is ``FromArgb(0, 0, 0)``.
    default_textcolor : System.Drawing.Color
        The default text color is ``FromArgb(255, 255, 255)``.

    Examples
    --------
    .. code-block:: python

        from random import randint
        from compas_rhino.conduits import LabelsConduit

        labels = [([1.0 * randint(0, 100), 1.0 * randint(0, 100), 0.0], str(i)) for i in range(100)]

        conduit = LabelsConduit(labels)

        with conduit.enabled():
            for i in range(100):
                conduit.labels = [([1.0 * randint(0, 100), 1.0 * randint(0, 100), 0.0], str(i)) for i in range(100)]
                conduit.redraw(pause=0.1)

    """

    default_color = FromArgb(0, 0, 0)
    default_textcolor = FromArgb(255, 255, 255)

    def __init__(self, labels, color=None, **kwargs):
        super(LabelsConduit, self).__init__(**kwargs)
        self._color = None
        self.labels = labels or []
        self.color = color

    @property
    def color(self):
        return self._colors

    @color.setter
    def color(self, color):
        if not color:
            return
        if not is_sequence_of_iterable(color[0]):
            # the first item in the list should be a tuple of colors
            # if not, wrap the tuple
            color = [color]
        color = [(FromArgb(* bg), FromArgb(* text)) for bg, text in iterable_like(self.labels, color, (self.default_color, self.default_textcolor))]
        self._color = color

    def DrawForeground(self, e):
        """Draw the labels as text dots.

        Parameters
        ----------
        e : Rhino.Display.DrawEventArgs

        Returns
        -------
        None

        """
        for i, (pos, text) in enumerate(self.labels):
            if self.color:
                color, textcolor = self.color[i]
                e.Display.DrawDot(Point3d(*pos), text, color, textcolor)
            else:
                e.Display.DrawDot(Point3d(*pos), text, self.default_color, self.default_textcolor)


# src/compas_rhino/conduits/lines.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from System.Drawing.Color import FromArgb

from Rhino.Geometry import Point3d

from compas.utilities import is_sequence_of_iterable
from compas.utilities import iterable_like
from .base import BaseConduit


class LinesConduit(BaseConduit):
    """A Rhino display conduit for lines.

    Parameters
    ----------
    lines : list[[point, point] or :class:`compas.geometry.Line`]
        A list of start-end point pairs that define the lines.
    thickness : list[int], optional
        The thickness of the individual lines.
        Default is :attr:`LinesConduit.default_thickness` for all lines.
    color : list[tuple[int, int, int]], optional
        The colors of the faces.
        Default is :attr:`LinesConduit.default_color` for all lines.

    Attributes
    ----------
    color : list[System.Drawing.Color]
        A color per line.
    thickness : list[float]
        A thickness per line.

    Class Attributes
    ----------------
    default_thickness : float
        The default thickness is ``1.0``.
    default_color : System.Drawing.Color
        the default color is ``FromArgb(255, 255, 255)``.

    Examples
    --------
    .. code-block:: python

        from random import randint

        points = [(1.0 * randint(0, 30), 1.0 * randint(0, 30), 0.0) for _ in range(100)]
        lines  = [(points[i], points[i + 1]) for i in range(99)]
        conduit = LinesConduit(lines)

        with conduit.enabled():
            for i in range(100):
                points = [(1.0 * randint(0, 30), 1.0 * randint(0, 30), 0.0) for _ in range(100)]
                conduit.lines = [(points[i], points[i + 1]) for i in range(99)]
                conduit.redraw(pause=0.1)

    """

    default_thickness = 1.0
    default_color = FromArgb(255, 255, 255)

    def __init__(self, lines, thickness=None, color=None, **kwargs):
        super(LinesConduit, self).__init__(**kwargs)
        self._thickness = None
        self._color = None
        self.lines = lines or []
        self.thickness = thickness
        self.color = color

    @property
    def thickness(self):
        return self._thickness

    @thickness.setter
    def thickness(self, thickness):
        thickness = thickness or self.default_thickness
        try:
            len(thickness)
        except TypeError:
            thickness = [thickness]
        thickness = iterable_like(self.lines, thickness, self.default_thickness)
        self._thickness = list(thickness)

    @property
    def color(self):
        return self._color

    @color.setter
    def color(self, color):
        color = color or self.default_color
        if not is_sequence_of_iterable(color):
            color = [color]
        self._color = [FromArgb(*c) for c in iterable_like(self.lines, color, self.default_color)]

    def DrawForeground(self, e):
        """Draw the lines.

        Parameters
        ----------
        e : Rhino.Display.DrawEventArgs

        Returns
        -------
        None

        """
        for (start, end), color, thickness in zip(self.lines, self.color, self.thickness):
            e.Display.DrawLine(Point3d(*start), Point3d(*end), color, thickness)


# src/compas_rhino/conduits/points.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from System.Drawing.Color import FromArgb

from Rhino.Geometry import Point3d
from Rhino.Display.PointStyle import Simple

from compas.utilities import color_to_rgb
from compas.utilities import iterable_like
from compas.utilities.coercing import is_sequence_of_iterable
from .base import BaseConduit


class PointsConduit(BaseConduit):
    """A Rhino display conduit for points.

    Parameters
    ----------
    points : list[[float, float, float] or :class:`compas.geometry.Point`]
        The coordinates of the points.
    size : list[int], optional
        The size of the points.
        Default is :attr:`PointsConduit.default_size` for all points.
    color : list[tuple[int, int, int]]
        The individual colors of the points.
        Default is :attr:`PointsConduit.default_color` for all points.

    Attributes
    ----------
    size : list[float]
        The size specification per point.
    color : list[System.Drawing.Color]
        The color per point.

    Class Attributes
    ----------------
    default_size : float
        The default size is ``3``.
    default_color : System.Drawing.Color
        The default color is ``FromArgb(255, 0, 0)``.

    Examples
    --------
    .. code-block:: python

        from random import randint
        from compas_rhino.conduits import PointsConduit

        points = [(1.0 * randint(0, 30), 1.0 * randint(0, 30), 0.0) for _ in range(100)]
        conduit = PointsConduit(points)

        with conduit.enabled():
            for i in range(100):
                conduit.points = [(1.0 * randint(0, 30), 1.0 * randint(0, 30), 0.0) for _ in range(100)]
                conduit.redraw(pause=0.1)

    """

    default_size = 3
    default_color = FromArgb(255, 0, 0)

    def __init__(self, points, size=None, color=None, **kwargs):
        super(PointsConduit, self).__init__(**kwargs)
        self._size = None
        self._color = None
        self.points = points or []
        self.size = size
        self.color = color

    @property
    def size(self):
        return self._size

    @size.setter
    def size(self, size):
        size = size or self.default_size
        try:
            len(size)
        except TypeError:
            size = [size]
        size = iterable_like(self.points, size, self.default_size)
        self._size = list(size)

    @property
    def color(self):
        return self._color

    @color.setter
    def color(self, color):
        color = color or self.default_color
        if not is_sequence_of_iterable(color):
            color = [color]
        color = [FromArgb(* color_to_rgb(c)) for c in iterable_like(self.points, color, self.default_color)]
        self._color = color

    def DrawForeground(self, e):
        """Draw the points.

        Parameters
        ----------
        e : Rhino.Display.DrawEventArgs

        Returns
        -------
        None

        """
        for xyz, size, color in zip(self.points, self.size, self.color):
            e.Display.DrawPoint(Point3d(*xyz), Simple, size, color)


# src/compas_rhino/conversions/__init__.py

"""
********************************************************************************
conversions
********************************************************************************

.. currentmodule:: compas_rhino.conversions

.. rst-class:: lead

Conversions between Rhino geometry objects (:mod:`Rhino.Geometry`) and COMPAS geometry objects (:mod:`compas.geometry`).

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    RhinoGeometry
    RhinoBox
    RhinoCircle
    RhinoCone
    RhinoCurve
    RhinoCylinder
    RhinoEllipse
    RhinoLine
    RhinoMesh
    RhinoPlane
    RhinoPoint
    RhinoPolyline
    RhinoSphere
    RhinoSurface
    RhinoVector


Exceptions
==========

.. autosummary::
    :toctree: generated/
    :nosignatures:

    ConversionError


Functions
=========

Primitives
----------

.. autosummary::
    :toctree: generated/
    :nosignatures:

    point_to_rhino
    vector_to_rhino
    line_to_rhino
    plane_to_rhino
    frame_to_rhino
    circle_to_rhino
    ellipse_to_rhino
    polyline_to_rhino
    polygon_to_rhino
    point_to_compas
    vector_to_compas
    line_to_compas
    plane_to_compas
    plane_to_compas_frame
    circle_to_compas
    ellipse_to_compas
    polyline_to_compas
    polygon_to_compas


Shapes
------

.. autosummary::
    :toctree: generated/
    :nosignatures:

    box_to_rhino
    sphere_to_rhino
    cone_to_rhino
    cylinder_to_rhino
    box_to_compas
    sphere_to_compas
    cone_to_compas
    cylinder_to_compas


Curves
------

.. autosummary::
    :toctree: generated/
    :nosignatures:

    line_to_rhino_curve
    circle_to_rhino_curve
    ellipse_to_rhino_curve
    curve_to_compas_line
    curve_to_compas_circle
    curve_to_compas_ellipse
    curve_to_compas_polyline


Surfaces
--------

.. autosummary::
    :toctree: generated/
    :nosignatures:


Transformations
---------------

.. autosummary::
    :toctree: generated/
    :nosignatures:

    xform_to_rhino

"""
from __future__ import absolute_import

from ._exceptions import ConversionError

from ._primitives import (
    point_to_rhino,
    vector_to_rhino,
    line_to_rhino,
    plane_to_rhino,
    frame_to_rhino,
    circle_to_rhino,
    ellipse_to_rhino,
    polyline_to_rhino,
    polygon_to_rhino,

    point_to_compas,
    vector_to_compas,
    line_to_compas,
    plane_to_compas,
    plane_to_compas_frame,
    circle_to_compas,
    ellipse_to_compas,
    polyline_to_compas,
    polygon_to_compas
)
from ._shapes import (
    box_to_rhino,
    sphere_to_rhino,
    cone_to_rhino,
    cylinder_to_rhino,

    box_to_compas,
    sphere_to_compas,
    cone_to_compas,
    cylinder_to_compas,
)
from ._curves import (
    line_to_rhino_curve,
    circle_to_rhino_curve,
    ellipse_to_rhino_curve,

    curve_to_compas_circle,
    curve_to_compas_ellipse,
    curve_to_compas_line,
    curve_to_compas_polyline
)
from ._transformations import (
    xform_to_rhino
)

from ._geometry import RhinoGeometry

from .box import RhinoBox
from .circle import RhinoCircle
from .cone import RhinoCone
from .curve import RhinoCurve
from .cylinder import RhinoCylinder
from .ellipse import RhinoEllipse
from .line import RhinoLine
from .mesh import RhinoMesh
from .plane import RhinoPlane
from .point import RhinoPoint
from .polyline import RhinoPolyline
from .sphere import RhinoSphere
from .surface import RhinoSurface
from .vector import RhinoVector

BaseRhinoGeometry = RhinoGeometry


__all__ = [
    'ConversionError',

    'point_to_rhino',
    'vector_to_rhino',
    'line_to_rhino',
    'plane_to_rhino',
    'frame_to_rhino',
    'circle_to_rhino',
    'ellipse_to_rhino',
    'polyline_to_rhino',
    'polygon_to_rhino',

    'point_to_compas',
    'vector_to_compas',
    'line_to_compas',
    'plane_to_compas',
    'plane_to_compas_frame',
    'circle_to_compas',
    'ellipse_to_compas',
    'polyline_to_compas',
    'polygon_to_compas',

    'box_to_rhino',
    'sphere_to_rhino',
    'cone_to_rhino',
    'cylinder_to_rhino',

    'box_to_compas',
    'sphere_to_compas',
    'cone_to_compas',
    'cylinder_to_compas',

    'line_to_rhino_curve',
    'circle_to_rhino_curve',
    'ellipse_to_rhino_curve',

    'curve_to_compas_circle',
    'curve_to_compas_ellipse',
    'curve_to_compas_line',
    'curve_to_compas_polyline',

    'xform_to_rhino',

    'RhinoGeometry',
    'RhinoBox',
    'RhinoCircle',
    'RhinoCone',
    'RhinoCurve',
    'RhinoCylinder',
    'RhinoEllipse',
    'RhinoLine',
    'RhinoMesh',
    'RhinoPlane',
    'RhinoPoint',
    'RhinoPolyline',
    'RhinoSphere',
    'RhinoSurface',
    'RhinoVector',
]


# src/compas_rhino/conversions/_curves.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.geometry import Line

from ._exceptions import ConversionError

from ._primitives import line_to_rhino
from ._primitives import circle_to_rhino
from ._primitives import ellipse_to_rhino
from ._primitives import point_to_compas
from ._primitives import circle_to_compas
from ._primitives import ellipse_to_compas
from ._primitives import polyline_to_compas

from Rhino.Geometry import NurbsCurve as RhinoNurbsCurve


def curve_to_compas_line(curve):
    """Convert a Rhino curve to a COMPAS line.

    Parameters
    ----------
    curve: :rhino:`Rhino.Geometry.Curve`

    Returns
    -------
    :class:`compas.geometry.Line`

    """
    return Line(point_to_compas(curve.PointAtStart),
                point_to_compas(curve.PointAtEnd))


def line_to_rhino_curve(line):
    """Convert a COMPAS line to a Rhino curve.

    Parameters
    ----------
    line: :class:`compas.geometry.Line`

    Returns
    -------
    :rhino:`Rhino.Geometry.Curve`

    """
    return RhinoNurbsCurve.CreateFromLine(line_to_rhino(line))


def curve_to_compas_circle(curve):
    """Convert a Rhino curve to a COMPAS circle.

    Parameters
    ----------
    curve: :rhino:`Rhino.Geometry.Curve`

    Returns
    -------
    :class:`compas.geometry.Circle`

    Raises
    ------
    ConversionError
        If the curve cannot be converted to a circle.

    """
    result, circle = curve.TryGetCircle()
    if not result:
        raise ConversionError('The curve cannot be converted to a circle.')
    return circle_to_compas(circle)


def circle_to_rhino_curve(circle):
    """Convert a COMPAS circle to a Rhino curve.

    Parameters
    ----------
    circle: :class:`compas.geometry.Circle`

    Returns
    -------
    :rhino:`Rhino.Geometry.Curve`

    """
    return RhinoNurbsCurve.CreateFromCircle(circle_to_rhino(circle))


def curve_to_compas_ellipse(curve):
    """Convert a Rhino curve to a COMPAS ellipse.

    Parameters
    ----------
    curve: :rhino:`Rhino.Geometry.Curve`

    Returns
    -------
    :class:`compas.geometry.Ellipse`

    Raises
    ------
    ConversionError
        If the curve cannot be converted to an ellipse.

    """
    result, ellipse = curve.TryGetEllipse()
    if not result:
        raise ConversionError('The curve cannot be converted to an ellipse.')
    return ellipse_to_compas(ellipse)


def ellipse_to_rhino_curve(ellipse):
    """Convert a COMPAS ellipse to a Rhino curve.

    Parameters
    ----------
    ellipse: :class:`compas.geometry.Ellipse`

    Returns
    -------
    :rhino:`Rhino.Geometry.Curve`

    """
    return RhinoNurbsCurve.CreateFromEllipse(ellipse_to_rhino(ellipse))


def curve_to_compas_polyline(curve):
    """Convert a Rhino curve to a COMPAS polyline.

    Parameters
    ----------
    curve: :rhino:`Rhino.Geometry.Curve`

    Returns
    -------
    :class:`compas.geometry.Polyline`

    Raises
    ------
    ConversionError
        If the curve cannot be converted to a polyline.

    """
    result, polyline = curve.TryGetPolyline()
    if not result:
        raise ConversionError('The curve cannot be converted to a polyline.')
    return polyline_to_compas(polyline)


def curve_to_compas_data(curve):
    """Convert a Rhino curve to a COMPAS data dict.

    Parameters
    ----------
    curve: :rhino:`Rhino.Geometry.Curve`

    Returns
    -------
    dict

    """
    nurbs = curve.ToNurbsCurve()
    points = []
    weights = []
    knots = []
    multiplicities = []
    degree = nurbs.Degree
    is_periodic = nurbs.IsPeriodic

    for index in range(nurbs.Points.Count):
        point = nurbs.Points.Item[index]
        points.append(point_to_compas(point.Location))
        weights.append(point.Weight)

    for index in range(nurbs.Knots.Count):
        knots.append(nurbs.Knots.Item[index])
        multiplicities.append(nurbs.Knots.KnotMultiplicity(index))

    return {
        'points': [point.data for point in points],
        'weights': weights,
        'knots': knots,
        'multiplicities': multiplicities,
        'degree': degree,
        'is_periodic': is_periodic
    }


def data_to_rhino_curve(data):
    """Convert a COMPAS curve to a Rhino curve.

    Parameters
    ----------
    data: dict

    Returns
    -------
    :rhino:`Rhino.Geometry.NurbsCurve`

    """
    nurbs = RhinoNurbsCurve(data['degree'], len(data['points']))

    for index, xyz in enumerate(data['points']):
        nurbs.Points.SetPoint(index, *xyz)

    knotvector = []
    for knot, mult in zip(data['knots'], data['multiplicities']):
        for i in range(mult):
            knotvector.append(knot)

    for index, knot in enumerate(knotvector):
        nurbs.Knots.Item[index] = knot
    return nurbs


# src/compas_rhino/conversions/_exceptions.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division


class ConversionError(Exception):
    """Raised when a conversion is not possible."""


# src/compas_rhino/conversions/_geometry.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino
import compas_rhino


class RhinoGeometry(object):
    """Base class for Rhino Geometry and DocObject wrappers.

    Attributes
    ----------
    name: str
        The name of the object.
    type: str
        The type of the object.
    guid: str
        The GUID of the object.
    object : :rhino:`Rhino_DocObjects_RhinoObject`
        A reference to the Rhino DocObject, if it exists.
    geometry: :rhino:`Rhino_Geometry_GeometryBase`
        A reference to the Rhino Geometry Object.

    """

    def __init__(self):
        super(RhinoGeometry, self).__init__()
        self._guid = None
        self._object = None
        self._geometry = None
        self._type = None
        self._name = None

    @property
    def guid(self):
        return self._guid

    @guid.setter
    def guid(self, guid):
        self.object = compas_rhino.find_object(guid)

    @property
    def object(self):
        return self._object

    @object.setter
    def object(self, obj):
        self._guid = obj.Id
        self._object = obj
        self.geometry = obj.Geometry

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        raise NotImplementedError

    @property
    def type(self):
        if self.object:
            return self.object.ObjectType
        return self._type

    @type.setter
    def type(self, value):
        self._type = value

    @property
    def name(self):
        if self.object:
            return self.object.Attributes.Name
        return self._name

    @name.setter
    def name(self, value):
        if self.object:
            self.object.Attributes.Name = value
            self.object.CommitChanges()
        else:
            self._name = value

    @classmethod
    def from_guid(cls, guid):
        """Try to construct a RhinoGeometry wrapper from the GUID of an existing Rhino DocObject.

        Parameters
        ----------
        guid : str
            The GUID of the Rhino DocObject.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoGeometry`
            The Rhino object wrapper.

        Raises
        ------
        :class:`ConversionError`
            If the geometry of the Rhino DocObject cannot be converted to the geometry type of the wrapper.

        """
        wrapper = cls()
        wrapper.guid = guid
        return wrapper

    @classmethod
    def from_object(cls, obj):
        """Construct a Rhino object wrapper from an existing Rhino object.

        Parameters
        ----------
        obj : :rhino:`Rhino_DocObjects_RhinoObject`
            The Rhino object.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoGeometry`
            The Rhino object wrapper.

        Raises
        ------
        :class:`ConversionError`
            If the geometry of the Rhino DocObject cannot be converted to the geometry type of the wrapper.

        """
        wrapper = cls()
        wrapper.object = obj
        return wrapper

    @classmethod
    def from_geometry(cls, geometry):
        """Construct a Rhino object wrapper from an existing Rhino object.

        Parameters
        ----------
        geometry : :rhino:`Rhino_DocObjects_RhinoObject`
            The Rhino object.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoGeometry`
            The Rhino object wrapper.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to the geometry type of the wrapper.

        """
        wrapper = cls()
        wrapper.geometry = geometry
        return wrapper

    def to_compas(self):
        raise NotImplementedError

    def transform(self, T):
        """Transform the Rhino object.

        Parameters
        ----------
        T : :class:`compas.geometry.Transformation` or :rhino:`Rhino.Geometry.Transform`
            The transformation matrix.

        Returns
        -------
        None
            The Rhino object is transformed in place.

        """
        if not isinstance(T, Rhino.Geometry.Transform):
            M = Rhino.Geometry.Transform(0.0)
            for i in range(4):
                for j in range(4):
                    M[i, j] = T[i, j]
        else:
            M = T
        self.geometry.Transform(M)


# src/compas_rhino/conversions/_primitives.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.geometry import Point
from compas.geometry import Vector
from compas.geometry import Line
from compas.geometry import Plane
from compas.geometry import Frame
from compas.geometry import Circle
from compas.geometry import Ellipse
from compas.geometry import Polyline
from compas.geometry import Polygon

from Rhino.Geometry import Point3d
from Rhino.Geometry import Vector3d
from Rhino.Geometry import Line as RhinoLine
from Rhino.Geometry import Plane as RhinoPlane
from Rhino.Geometry import Circle as RhinoCircle
from Rhino.Geometry import Ellipse as RhinoEllipse
from Rhino.Geometry import Polyline as RhinoPolyline


def point_to_compas(point):
    """Convert a Rhino point to a COMPAS point.

    Parameters
    ----------
    point : :rhino:`Rhino.Geometry.Point3d`

    Returns
    -------
    :class:`compas.geometry.Point`

    """
    return Point(point.X, point.Y, point.Z)


def point_to_rhino(point):
    """Convert a COMPAS point to a Rhino point.

    Parameters
    ----------
    point : :class:`compas.geometry.Point`

    Returns
    -------
    :rhino:`Rhino.Geometry.Point3d`

    """
    return Point3d(point[0], point[1], point[2])


def vector_to_compas(vector):
    """Convert a Rhino vector to a COMPAS vector.

    Parameters
    ----------
    vector : :rhino:`Rhino.Geometry.Vector3d`

    Returns
    -------
    :class:`compas.geometry.Vector`

    """
    return Vector(vector.X, vector.Y, vector.Z)


def vector_to_rhino(vector):
    """Convert a COMPAS vector to a Rhino vector.

    Parameters
    ----------
    vector : :class:`compas.geometry.Vector`

    Returns
    -------
    :rhino:`Rhino.Geometry.Vector3d`

    """
    return Vector3d(vector[0], vector[1], vector[2])


def line_to_compas(line):
    """Convert a Rhino line to a COMPAS line.

    Parameters
    ----------
    line : :rhino:`Rhino.Geometry.Line`

    Returns
    -------
    :class:`compas.geometry.Line`

    """
    return Line(point_to_compas(line.From), point_to_compas(line.To))


def line_to_rhino(line):
    """Convert a COMPAS line to a Rhino line.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`

    Returns
    -------
    :rhino:`Rhino.Geometry.Line`

    """
    return RhinoLine(point_to_rhino(line[0]), point_to_rhino(line[1]))


def plane_to_compas(plane):
    """Convert a Rhino plane to a COMPAS plane.

    Parameters
    ----------
    plane : :rhino:`Rhino.Geometry.Plane`

    Returns
    -------
    :class:`compas.geometry.Plane`

    """
    return Plane(point_to_compas(plane.Origin), vector_to_compas(plane.Normal))


def plane_to_rhino(plane):
    """Convert a COMPAS plane to a Rhino plane.

    Parameters
    ----------
    plane : :class:`compas.geometry.Plane`

    Returns
    -------
    :rhino:`Rhino.Geometry.Plane`

    """
    return RhinoPlane(point_to_rhino(plane[0]), vector_to_rhino(plane[1]))


def plane_to_compas_frame(plane):
    """Convert a Rhino plane to a COMPAS frame.

    Parameters
    ----------
    plane : :rhino:`Rhino.Geometry.Plane`

    Returns
    -------
    :class:`compas.geometry.Frame`

    """
    return Frame(point_to_compas(plane.Origin), vector_to_compas(plane.XAxis), vector_to_compas(plane.YAxis))


def frame_to_rhino(frame):
    """Convert a COMPAS frame to a Rhino plane.

    Parameters
    ----------
    frame : :class:`compas.geometry.Frame`

    Returns
    -------
    :rhino:`Rhino.Geometry.Plane`

    """
    return RhinoPlane(point_to_rhino(frame[0]), vector_to_rhino(frame[1]), vector_to_rhino(frame[2]))


def circle_to_compas(circle):
    """Convert a Rhino circle to a COMPAS circle.

    Parameters
    ----------
    circle : :rhino:`Rhino.Geometry.Circle`

    Returns
    -------
    :class:`compas.geometry.Circle`

    """
    return Circle(plane_to_compas(circle.Plane), circle.Radius)


def circle_to_rhino(circle):
    """Convert a COMPAS circle to a Rhino circle.

    Parameters
    ----------
    circle : :class:`compas.geometry.Circle`

    Returns
    -------
    :rhino:`Rhino.Geometry.Circle`

    """
    return RhinoCircle(plane_to_rhino(circle[0]), circle[1])


def ellipse_to_compas(ellipse):
    """Convert a Rhino ellipse to a COMPAS ellipse.

    Parameters
    ----------
    ellipse : :rhino:`Rhino.Geometry.Ellipse`

    Returns
    -------
    :class:`compas.geometry.Ellipse`

    """
    return Ellipse(plane_to_compas(ellipse.Plane), ellipse.Major, ellipse.Minor)


def ellipse_to_rhino(ellipse):
    """Convert a COMPAS ellipse to a Rhino ellipse.

    Parameters
    ----------
    ellipse : :class:`compas.geometry.Ellipse`

    Returns
    -------
    :rhino:`Rhino.Geometry.Ellipse`

    """
    return RhinoEllipse(plane_to_rhino(ellipse[0]), ellipse[1], ellipse[2])


def polyline_to_compas(polyline):
    """Convert a Rhino polyline to a COMPAS polyline.

    Parameters
    ----------
    polyline : :rhino:`Rhino.Geometry.Polyline`

    Returns
    -------
    :class:`compas.geometry.Polyline`

    """
    return Polyline([point_to_compas(point) for point in polyline])


def polyline_to_rhino(polyline):
    """Convert a COMPAS polyline to a Rhino polyline.

    Parameters
    ----------
    polyline : :class:`compas.geometry.Polyline`

    Returns
    -------
    :rhino:`Rhino.Geometry.Ellipse`

    """
    return RhinoPolyline([point_to_rhino(point) for point in polyline])


def polygon_to_compas(polygon):
    """Convert a Rhino polygon to a COMPAS polygon.

    Parameters
    ----------
    polygon : :rhino:`Rhino.Geometry.Polygon`

    Returns
    -------
    :class:`compas.geometry.Polygon`

    """
    return Polygon([point_to_compas(point) for point in polygon])


def polygon_to_rhino(polygon):
    """Convert a COMPAS polygon to a Rhino polygon.

    Parameters
    ----------
    polygon : :class:`compas.geometry.Polygon`

    Returns
    -------
    :rhino:`Rhino.Geometry.Polygon`

    """
    raise NotImplementedError


# src/compas_rhino/conversions/_shapes.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.geometry import Plane
from compas.geometry import Circle
from compas.geometry import Box
from compas.geometry import Sphere
from compas.geometry import Cone
from compas.geometry import Cylinder

from Rhino.Geometry import Box as RhinoBox
from Rhino.Geometry import Sphere as RhinoSphere
from Rhino.Geometry import Cone as RhinoCone
from Rhino.Geometry import Cylinder as RhinoCylinder

from ._primitives import plane_to_rhino
from ._primitives import circle_to_rhino
from ._primitives import frame_to_rhino
from ._primitives import point_to_rhino
from ._primitives import plane_to_compas_frame
from ._primitives import plane_to_compas
from ._primitives import point_to_compas
from ._primitives import vector_to_compas


def box_to_compas(box):
    """Convert a Rhino box to a COMPAS box.

    Parameters
    ----------
    box: :rhino:`Rhino.Geometry.Box`

    Returns
    -------
    :class:`compas.geometry.Box`

    """
    xsize = box.X.Length
    ysize = box.Y.Length
    zsize = box.Z.Length
    frame = plane_to_compas_frame(box.Plane)
    frame.point.x += 0.5 * xsize
    frame.point.y += 0.5 * ysize
    frame.point.z += 0.5 * zsize
    return Box(frame, xsize, ysize, zsize)


def box_to_rhino(box):
    """Convert a COMPAS box to a Rhino box.

    Parameters
    ----------
    box: :class:`compas.geometry.Box`

    Returns
    -------
    :rhino:`Rhino.Geometry.Box`

    """
    return RhinoBox(frame_to_rhino(box.frame), box.xsize, box.ysize, box.zsize)


def sphere_to_compas(sphere):
    """Convert a Rhino sphere to a COMPAS sphere.

    Parameters
    ----------
    sphere: :rhino:`Rhino.Geometry.Sphere`

    Returns
    -------
    :class:`compas.geometry.Sphere`

    """
    return Sphere(point_to_compas(sphere.Center), sphere.Radius)


def sphere_to_rhino(sphere):
    """Convert a COMPAS sphere to a Rhino sphere.

    Parameters
    ----------
    sphere: :class:`compas.geometry.Sphere`

    Returns
    -------
    :rhino:`Rhino.Geometry.Sphere`

    """
    return RhinoSphere(point_to_rhino(sphere.point), sphere.radius)


def cone_to_compas(cone):
    """Convert a Rhino cone to a COMPAS cone.

    Parameters
    ----------
    cone: :rhino:`Rhino.Geometry.Cone`

    Returns
    -------
    :class:`compas.geometry.Cone`

    """
    plane = Plane(cone.BasePoint, vector_to_compas(cone.Plane.Normal).inverted())
    return Cone(Circle(plane, cone.Radius), cone.Height)


def cone_to_rhino(cone):
    """Convert a COMPAS cone to a Rhino cone.

    Parameters
    ----------
    cone: :class:`compas.geometry.Cone`

    Returns
    -------
    :rhino:`Rhino.Geometry.Cone`

    """
    return RhinoCone(plane_to_rhino(cone.circle.plane), cone.height, cone.circle.radius)


def cylinder_to_compas(cylinder):
    """Convert a Rhino cylinder to a COMPAS cylinder.

    Parameters
    ----------
    cylinder: :rhino:`Rhino.Geometry.Cylinder`

    Returns
    -------
    :class:`compas.geometry.Cylinder`

    """
    plane = plane_to_compas(cylinder.BasePlane)
    height = cylinder.TotalHeight
    plane.point += plane.normal * (0.5 * height)
    return Cylinder(Circle(plane, cylinder.Radius), height)


def cylinder_to_rhino(cylinder):
    """Convert a COMPAS cylinder to a Rhino cylinder.

    Parameters
    ----------
    cylinder: :class:`compas.geometry.Cylinder`

    Returns
    -------
    :rhino:`Rhino.Geometry.Cylinder`

    """
    return RhinoCylinder(circle_to_rhino(cylinder.circle), cylinder.height)


# src/compas_rhino/conversions/_surfaces.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.geometry import Point

from Rhino.Geometry import NurbsSurface as RhinoNurbsSurface

from ._primitives import point_to_rhino
from ._primitives import point_to_compas


def surface_to_compas_data(surface):
    """Convert a Rhino surface to a COMPAS surface.

    Parameters
    ----------
    surface: :rhino:`Rhino.Geometry.Surface`

    Returns
    -------
    dict

    """
    surface = surface.ToNurbsSurface()

    points = []
    weights = []
    for j in range(surface.Points.VCount):
        _points = []
        _weights = []
        for i in range(surface.Points.UCount):
            point = surface.Points.GetPoint(i, j)
            weight = surface.Points.GetWeight(i, j)
            _points.append(point_to_compas(point))
            _weights.append(weight)
        points.append(_points)
        weights.append(_weights)

    u_knots = []
    u_mults = []
    for index in range(surface.KnotsU.Count):
        u_knots.append(surface.KnotsU.Item[index])
        u_mults.append(surface.KnotsU.KnotMultiplicity(index))

    v_knots = []
    v_mults = []
    for index in range(surface.KnotsV.Count):
        v_knots.append(surface.KnotsV.Item[index])
        v_mults.append(surface.KnotsV.KnotMultiplicity(index))

    u_degree = surface.OrderU - 1
    v_degree = surface.OrderV - 1

    is_u_periodic = False
    is_v_periodic = False

    return {
        'points': [[point.data for point in row] for row in points],
        'weights': weights,
        'u_knots': u_knots,
        'v_knots': v_knots,
        'u_mults': u_mults,
        'v_mults': v_mults,
        'u_degree': u_degree,
        'v_degree': v_degree,
        'is_u_periodic': is_u_periodic,
        'is_v_periodic': is_v_periodic
    }


def data_to_rhino_surface(data):
    """Convert a COMPAS surface to a Rhino surface.

    Parameters
    ----------
    data: dict

    Returns
    -------
    :rhino:`Rhino.Geometry.NurbsSurface`

    """
    points = [[Point.from_data(point) for point in row] for row in data['points']]

    nu = len(points[0])
    nv = len(points)

    nurbs = RhinoNurbsSurface.Create(3,
                                     False,
                                     data['u_degree'] + 1,
                                     data['v_degree'] + 1,
                                     nu,
                                     nv)
    for i in range(nu):
        for j in range(nv):
            nurbs.Points.SetPoint(i, j, point_to_rhino(points[j][i]))
            nurbs.Points.SetWeight(i, j, data['weights'][j][i])

    u_knotvector = []
    for knot, mult in zip(data['u_knots'], data['u_mults']):
        for i in range(mult):
            u_knotvector.append(knot)

    for index, knot in enumerate(u_knotvector):
        nurbs.KnotsU.Item[index] = knot

    v_knotvector = []
    for knot, mult in zip(data['v_knots'], data['v_mults']):
        for i in range(mult):
            v_knotvector.append(knot)

    for index, knot in enumerate(v_knotvector):
        nurbs.KnotsV.Item[index] = knot

    return nurbs


# src/compas_rhino/conversions/_transformations.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from Rhino.Geometry import Transform


def xform_from_transformation(transformation):
    """Creates a Rhino transformation from a COMPAS transformation.

    Parameters
    ----------
    transformation : :class:`compas.geometry.Transformation`
        COMPAS transformation.

    Returns
    -------
    :rhino:`Rhino.Geometry.Transform`

    """
    transform = Transform(1.0)
    for i in range(0, 4):
        for j in range(0, 4):
            transform[i, j] = transformation[i, j]
    return transform


xform_to_rhino = xform_from_transformation


def xform_from_transformation_matrix(matrix):
    """Creates a Rhino transformation from a 4x4 transformation matrix.

    Parameters
    ----------
    matrix : list[list[float]]
        The 4x4 transformation matrix in row-major order.

    Returns
    -------
    :rhino:`Rhino.Geometry.Transform`

    """
    transform = Transform(1.0)
    for i in range(0, 4):
        for j in range(0, 4):
            transform[i, j] = matrix[i][j]
    return transform


xform_matrix_to_rhino = xform_from_transformation_matrix


def xtransform(geometry, transformation):
    """Transforms the Rhino Geometry object with a COMPAS transformation.

    Parameters
    ----------
    geometry : :rhino:`Rhino.Geometry.GeometryBase`
        Rhino Geometry object.
    transformation : :class:`compas.geometry.Transformation`
        COMPAS transformation.

    Returns
    -------
    None

    """
    T = xform_from_transformation(transformation)
    geometry.Transform(T)


def xtransformed(geometry, transformation):
    """Returns a copy of the transformed Rhino Geometry object.

    Parameters
    ----------
    geometry : :rhino:`Rhino.Geometry.GeometryBase`
        Rhino Geometry object.
    transformation : :class:`compas.geometry.Transformation`
        COMPAS transformation.

    Returns
    -------
    :rhino:`Rhino.Geometry.GeometryBase`

    """
    T = xform_from_transformation(transformation)
    geometry = geometry.Duplicate()
    geometry.Transform(T)
    return geometry


# src/compas_rhino/conversions/box.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from compas.geometry import Box

from ._exceptions import ConversionError

from ._shapes import box_to_rhino
from ._shapes import box_to_compas

from ._geometry import RhinoGeometry


class RhinoBox(RhinoGeometry):
    """Wrapper for Rhino boxes."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Box` or :class:`compas.geometry.Box`
            The geometry object defining a box.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a box.
        """
        if not isinstance(geometry, Rhino.Geometry.Box):
            if isinstance(geometry, Rhino.Geometry.Extrusion):
                plane = geometry.GetPathPlane(0)
                box = geometry.GetBoundingBox(plane)
                geometry = Rhino.Geometry.Box(plane, box)
            elif isinstance(geometry, Box):
                geometry = box_to_rhino(geometry)
            else:
                raise ConversionError('Geometry object cannot be converted to a box: {}'.format(geometry))

        self._geometry = geometry

    def to_compas(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Box`
            A COMPAS box.
        """
        return box_to_compas(self.geometry)


# src/compas_rhino/conversions/circle.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from ._primitives import circle_to_compas
from ._primitives import circle_to_rhino

from ._geometry import RhinoGeometry


class RhinoCircle(RhinoGeometry):
    """Wrapper for Rhino circles."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Circle` or :class:`compas.geometry.Circle`
            The geometry object defining a circle.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a box.
        """
        if not isinstance(geometry, Rhino.Geometry.Circle):
            geometry = circle_to_rhino(geometry)
        self._geometry = geometry

    def to_compas(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Circle`
            A COMPAS circle.
        """
        return circle_to_compas(self.geometry)


# src/compas_rhino/conversions/cone.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from compas.geometry import Cone

from ._exceptions import ConversionError

from ._shapes import cone_to_rhino
from ._shapes import cone_to_compas

from ._geometry import RhinoGeometry


class RhinoCone(RhinoGeometry):
    """Wrapper for Rhino cones."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Cone` or :class:`compas.geometry.Cone`
            The geometry object defining a cone.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a cone.
        """
        if not isinstance(geometry, Rhino.Geometry.Cone):
            if isinstance(geometry, Rhino.Geometry.Brep):
                if geometry.Faces.Count > 2:
                    raise ConversionError('Object brep cannot be converted to a cone.')
                faces = geometry.Faces
                geometry = None
                for face in faces:
                    if face.IsCone():
                        result, geometry = face.TryGetCone()
                        if result:
                            break
                if not geometry:
                    raise ConversionError('Object brep cannot be converted to a cone.')
            elif isinstance(geometry, Cone):
                geometry = cone_to_rhino(geometry)
            else:
                raise ConversionError('Geometry object cannot be converted to a cone: {}'.format(geometry))
        self._geometry = geometry

    def to_compas(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Cone`
            A COMPAS cone.
        """
        return cone_to_compas(self.geometry)


# src/compas_rhino/conversions/curve.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from compas.geometry import Line
from compas.geometry import Circle
from compas.geometry import Ellipse

from ._exceptions import ConversionError

from ._curves import circle_to_rhino_curve
from ._curves import ellipse_to_rhino_curve
from ._curves import line_to_rhino_curve
from ._curves import curve_to_compas_circle
from ._curves import curve_to_compas_ellipse
from ._curves import curve_to_compas_polyline
from ._curves import curve_to_compas_line

from ._geometry import RhinoGeometry


class RhinoCurve(RhinoGeometry):
    """Wrapper for Rhino curves."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Curve` or :class:`compas.geometry.Line` or :class:`compas.geometry.Circle` or :class:`compas.geometry.Ellipse`
            The geometry object defining a curve.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a curve.
        """
        if not isinstance(geometry, Rhino.Geometry.Curve):
            if isinstance(geometry, Line):
                geometry = line_to_rhino_curve(geometry)
            elif isinstance(geometry, Circle):
                geometry = circle_to_rhino_curve(geometry)
            elif isinstance(geometry, Ellipse):
                geometry = ellipse_to_rhino_curve(geometry)
            else:
                raise ConversionError('The geometry cannot be converted to a curve.')
        self._geometry = geometry

    def to_compas(self):
        """Convert the curve to a COMPAS curve.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`
        """
        from compas.geometry import NurbsCurve
        curve = NurbsCurve()
        curve.rhino_curve = self.geometry
        return curve

    def to_compas_circle(self):
        """Convert the curve to a COMPAS circle.

        Returns
        -------
        :class:`compas.geometry.Circle`
        """
        return curve_to_compas_circle(self.geometry)

    def to_compas_ellipse(self):
        """Convert the curve to a COMPAS ellipse.

        Returns
        -------
        :class:`compas.geometry.Ellipse`
        """
        return curve_to_compas_ellipse(self.geometry)

    def to_compas_line(self):
        """Convert the curve to a COMPAS line.

        Returns
        -------
        :class:`compas.geometry.Line`
        """
        return curve_to_compas_line(self.geometry)

    def to_compas_polyline(self):
        """Convert the curve to a COMPAS polyline.

        Returns
        -------
        :class:`compas.geometry.Polyline`
        """
        return curve_to_compas_polyline(self.geometry)

    def closest_point(self, point, maxdist=0.0, return_param=False):
        """Compute the closest point on a curve to a point in space.

        Parameters
        ----------
        point : point
            A point location.
        maxdist : float, optional
            The maximum distance between the point on the curve and the curve.
            Default is ``0.0``.
        return_param : bool, optional
            Return not only the point coordinates, but also the parameter of the point on the curve.
            Default is False.

        Returns
        -------
        list
            The XYZ coordinates of the closest point, if ``return_param`` is False.
            The XYZ coordinates of the closest point and the curve parameter, if ``return_param`` is True.

        """
        rc, t = self.geometry.ClosestPoint(Rhino.Geometry.Point3d(*point), maxdist)
        x, y, z = list(self.geometry.PointAt(t))
        if not return_param:
            return [x, y, z]
        return [x, y, z, t]

    def closest_points(self, points, maxdist=0.0):
        """Compute the closest points on the curve to a list of point locations.

        Parameters
        ----------
        points : list
            The point locations.
        maxdist : float, optional
            The maximum distance between the closest points and the curve.
            Default is ``0.0``.

        Returns
        -------
        list
            A list of closest point locations.
        """
        return [self.closest_point(point, maxdist) for point in points]


# src/compas_rhino/conversions/cylinder.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from compas.geometry import Cylinder

from ._exceptions import ConversionError

from ._shapes import cylinder_to_rhino
from ._shapes import cylinder_to_compas

from ._geometry import RhinoGeometry


class RhinoCylinder(RhinoGeometry):
    """Wrapper for Rhino cylinders."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Cylinder` or :class:`compas.geometry.Cylinder`
            The geometry object defining a cylinder.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a cylinder.
        """
        if not isinstance(geometry, Rhino.Geometry.Cylinder):
            if isinstance(geometry, Rhino.Geometry.Extrusion):
                geometry = geometry.ToBrep()
            if isinstance(geometry, Rhino.Geometry.Brep):
                if geometry.Faces.Count > 3:
                    raise ConversionError('Object brep cannot be converted to a cylinder.')
                faces = geometry.Faces
                geometry = None
                for face in faces:
                    if face.IsCylinder():
                        result, geometry = face.TryGetFiniteCylinder(0.001)
                        if result:
                            break
                if not geometry:
                    raise ConversionError('Object brep cannot be converted to a cylinder.')
            elif isinstance(geometry, Cylinder):
                geometry = cylinder_to_rhino(geometry)
            else:
                raise ConversionError('Geometry object cannot be converted to a cylinder: {}'.format(geometry))
        self._geometry = geometry

    def to_compas(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Cylinder`
            A COMPAS cylinder.
        """
        return cylinder_to_compas(self.geometry)


# src/compas_rhino/conversions/ellipse.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from ._primitives import ellipse_to_compas
from ._primitives import ellipse_to_rhino

from ._geometry import RhinoGeometry


class RhinoEllipse(RhinoGeometry):
    """Wrapper for Rhino ellipses."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Ellipse` or :class:`compas.geometry.Ellipse`
            The geometry object defining an ellipse.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to an ellipse.
        """
        if not isinstance(geometry, Rhino.Geometry.Ellipse):
            geometry = ellipse_to_rhino(geometry)
        self._geometry = geometry

    def to_compas(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Ellipse`
            A COMPAS ellipse.
        """
        return ellipse_to_compas(self.geometry)


# src/compas_rhino/conversions/line.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from ._primitives import line_to_compas
from ._primitives import line_to_rhino

from ._curves import curve_to_compas_line

from ._geometry import RhinoGeometry


class RhinoLine(RhinoGeometry):
    """Wrapper for Rhino lines."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Line` or :class:`compas.geometry.Line`
            The input geometry.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a line.
        """
        if not isinstance(geometry, Rhino.Geometry.Line):
            if isinstance(geometry, Rhino.Geometry.Curve):
                geometry = curve_to_compas_line(geometry)
            geometry = line_to_rhino(geometry)
        self._geometry = geometry

    def to_compas(self):
        """Convert the line to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Line`
            A COMPAS line.
        """
        return line_to_compas(self.geometry)


# src/compas_rhino/conversions/mesh.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from compas.datastructures import Mesh

from ._geometry import RhinoGeometry


class RhinoMesh(RhinoGeometry):
    """Wrapper for Rhino meshes."""

    @property
    def vertices(self):
        if self.geometry:
            vertices = [[point.X, point.Y, point.Z] for point in self.geometry.Vertices]
        else:
            vertices = []
        return vertices

    @property
    def faces(self):
        if self.geometry:
            faces = [[face.A, face.B, face.C] if face.IsTriangle else [face.A, face.B, face.C, face.D] for face in self.geometry.Faces]
        else:
            faces = []
        return faces

    @property
    def vertexnormals(self):
        if self.geometry:
            # self.geometry.ComputeNormals()
            # self.geometry.UnitizeNormals()
            normals = [[vector.X, vector.Y, vector.Z] for vector in self.geometry.Normals]
        else:
            normals = []
        return normals

    @property
    def facenormals(self):
        if self.geometry:
            # self.geometry.ComputeFaceNormals()
            # self.geometry.UnitizeFaceNormals()
            normals = [[vector.X, vector.Y, vector.Z] for vector in self.geometry.FaceNormals]
        else:
            normals = []
        return normals

    @property
    def vertexcolors(self):
        if self.geometry:
            colors = [[color.R, color.G, color.B] for color in self.geometry.VertexColors]
        else:
            colors = []
        return colors

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry: :rhino:`Rhino_Geometry_Mesh`
            A Rhino mesh geometry.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a mesh.
        """
        self._geometry = geometry

    def to_compas(self, cls=None):
        """Convert a Rhino mesh to a COMPAS mesh.

        Parameters
        ----------
        cls: :class:`compas.datastructures.Mesh`, optional
            The mesh type.

        Returns
        -------
        :class:`compas.datastructures.Mesh`
            The equivalent COMPAS mesh.
        """
        cls = cls or Mesh
        mesh = cls()

        for vertex in self.geometry.Vertices:
            mesh.add_vertex(attr_dict=dict(x=float(vertex.X), y=float(vertex.Y), z=float(vertex.Z)))

        for face in self.geometry.Faces:
            if face.A == face.D or face.C == face.D:
                mesh.add_face([face.A, face.B, face.C])
            else:
                mesh.add_face([face.A, face.B, face.C, face.D])

        mesh.name = self.name

        return mesh

    def closest_point(self, point, maxdist=0.0):
        """Compute the closest point on the mesh to a given point.

        Parameters
        ----------
        point: point
            A point location.
        maxdist: float, optional
            The maximum distance between the closest point and the mesh.
            Default is ``0.0``.

        Returns
        -------
        list
            The XYZ coordinates of the closest point.
        """
        face, point = self.geometry.ClosestPoint(Rhino.Geometry.Point3d(*point), maxdist)
        return list(point)

    def closest_points(self, points, maxdist=None):
        """Compute the closest points on the mesh to a list of input points.

        Parameters
        ----------
        points : list of point
            The input points.
        maxdist : float, optional
            The maximum distance between the closest point and the mesh.
            Default is ``0.0``.

        Returns
        -------
        list of point
            The XYZ coordinates of the closest points.
        """
        return [self.closest_point(point, maxdist) for point in points]


# src/compas_rhino/conversions/plane.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from compas.geometry import Frame
from ._primitives import plane_to_rhino
from ._primitives import frame_to_rhino
from ._primitives import plane_to_compas
from ._primitives import plane_to_compas_frame

from ._geometry import RhinoGeometry


class RhinoPlane(RhinoGeometry):
    """Wrapper for Rhino planes."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Plane` or :class:`compas.geometry.Plane` or :class:`compas.geometry.Frame`
            The geometry object defining a plane.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a plane.

        """
        if not isinstance(geometry, Rhino.Geometry.Plane):
            if isinstance(geometry, Frame):
                geometry = frame_to_rhino(geometry)
            else:
                geometry = plane_to_rhino(geometry)
        self._geometry = geometry

    def to_compas(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Plane`
            A COMPAS plane.
        """
        return plane_to_compas(self.geometry)

    def to_compas_frame(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Frame`
            A COMPAS frame.
        """
        return plane_to_compas_frame(self.geometry)


# src/compas_rhino/conversions/point.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from ._primitives import point_to_rhino
from ._primitives import point_to_compas

from .vector import RhinoVector


class RhinoPoint(RhinoVector):
    """Wrapper for Rhino points."""

    @property
    def object(self):
        return self._object

    @object.setter
    def object(self, obj):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        obj : :rhino:`Rhino_DocObjects_PointObject`
            The input object.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a point.

        """
        self._guid = obj.Id
        self._object = obj
        self.geometry = obj.Geometry.Location

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Point3d` or :class:`compas.geometry.Point` or list of float
            The input geometry.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a plane.

        """
        if not isinstance(geometry, Rhino.Geometry.Point3d):
            geometry = point_to_rhino(geometry)
        self._geometry = geometry

    def to_compas(self):
        """Convert the wrapper to a COMPAS point.

        Returns
        -------
        :class:`compas.geometry.Point`
            A COMPAS point.
        """
        return point_to_compas(self.geometry)

    def closest_point(self, point, maxdist=0.0, return_param=False):
        """Compute the closest point to a point in space.

        Parameters
        ----------
        point : point
            A point location.
        maxdist : float, optional
            The maximum distance between the point on the curve and the curve.
            Default is ``0.0``.
        return_param : bool, optional
            Return not only the point coordinates, but also the parameter of the point on the curve.
            Default is False.

        Returns
        -------
        tuple
            The XYZ coordinates of the point.

        """
        return self.geometry.X, self.geometry.Y, self.geometry.Z


# src/compas_rhino/conversions/polyline.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from ._primitives import polyline_to_compas
from ._primitives import polyline_to_rhino

from ._geometry import RhinoGeometry


class RhinoPolyline(RhinoGeometry):
    """Wrapper for Rhino polylines."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Polyline` or :class:`compas.geometry.Polyline` or list of points
            The input geometry.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a polyline.
        """
        if not isinstance(geometry, Rhino.Geometry.Polyline):
            geometry = polyline_to_rhino(geometry)
        self._geometry = geometry

    def to_compas(self):
        """Convert the polyline to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Polyline`
            A COMPAS polyline.
        """
        return polyline_to_compas(self.geometry)


# src/compas_rhino/conversions/sphere.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino

from compas.geometry import Sphere

from ._exceptions import ConversionError

from ._shapes import sphere_to_rhino
from ._shapes import sphere_to_compas

from ._geometry import RhinoGeometry


class RhinoSphere(RhinoGeometry):
    """Wrapper for Rhino spheres."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Sphere` or :class:`compas.geometry.Sphere`
            The geometry object defining a sphere.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a sphere.
        """
        if not isinstance(geometry, Rhino.Geometry.Sphere):
            if isinstance(geometry, Rhino.Geometry.Brep):
                if geometry.Faces.Count != 1:
                    raise ConversionError('Object brep cannot be converted to a sphere.')
                face = geometry.Faces.Item[0]
                if not face.IsSphere():
                    raise ConversionError('Object brep cannot be converted to a sphere.')
                result, geometry = face.TryGetSphere()
                if not result:
                    raise ConversionError('Object brep cannot be converted to a sphere.')
            elif isinstance(geometry, Sphere):
                geometry = sphere_to_rhino(geometry)
            else:
                raise ConversionError('Geometry object cannot be converted to a sphere: {}'.format(geometry))
        self._geometry = geometry

    def to_compas(self):
        """Convert to a COMPAS geometry object.

        Returns
        -------
        :class:`compas.geometry.Sphere`
            A COMPAS sphere.
        """
        return sphere_to_compas(self.geometry)


# src/compas_rhino/conversions/surface.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import Rhino
import compas_rhino

from compas.utilities import memoize, geometric_key
from compas.datastructures import Mesh
from compas.datastructures import meshes_join

from ._primitives import point_to_compas

from ._geometry import RhinoGeometry
from ._exceptions import ConversionError


class RhinoSurface(RhinoGeometry):
    """Wrapper for Rhino surfaces."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Surface`
            The geometry object defining a surface.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a surface.
        """
        if not isinstance(geometry, Rhino.Geometry.Surface):
            if not isinstance(geometry, Rhino.Geometry.Brep):
                raise TypeError("Geometry cannot be interpreted as a `Rhino.Geometry.Surface`: {}".format(type(geometry)))
        self._geometry = geometry

    def to_compas(self):
        """Convert the surface to a COMPAS surface.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsSurface`

        Raises
        ------
        :class:`ConversionError`
            If the surface BRep contains more than one face.
        """
        from compas.geometry import NurbsSurface

        brep = Rhino.Geometry.Brep.TryConvertBrep(self.geometry)
        surface = NurbsSurface()
        if brep.Surfaces.Count > 1:
            raise ConversionError('Conversion of a BRep with multiple underlying surface is currently not supported.')
        for geometry in brep.Surfaces:
            surface.rhino_surface = geometry
            break
        return surface

    def to_compas_mesh(self, cls=None, facefilter=None, cleanup=False):
        """Convert the surface b-rep loops to a COMPAS mesh.

        Parameters
        ----------
        cls : :class:`compas.datastructures.Mesh`, optional
            The type of COMPAS mesh.
        facefilter : callable, optional
            A filter for selection which Brep faces to include.
            If provided, the filter should return True or False per face.
            A very simple filter that includes all faces is ``def facefilter(face): return True``.
            Default parameter value is None in which case all faces are included.
        cleanup : bool, optional
            Flag indicating to clean up the result.
            Cleaning up means to remove isolated faces and unused vertices.
            Default is False.

        Returns
        -------
        :class:`compas.datastructures.Mesh`
            The resulting mesh.

        Examples
        --------
        >>> import compas_rhino
        >>> from compas_rhino.geometry import RhinoSurface
        >>> from compas_rhino.artists import MeshArtist

        >>> def facefilter(face):
        ...     success, w, h = face.GetSurfaceSize()
        ...     if success:
        ...         if w > 10 and h > 10:
        ...             return True
        ...     return False
        ...

        >>> guid = compas_rhino.select_surface()
        >>> surf = RhinoSurface.from_guid(guid)
        >>> mesh = surf.to_compas(facefilter=facefilter)

        >>> artist = MeshArtist(mesh, layer="Blocks")
        >>> artist.clear_layer()
        >>> artist.draw()

        """
        if not self.geometry.HasBrepForm:
            return
        brep = Rhino.Geometry.Brep.TryConvertBrep(self.geometry)
        if facefilter and callable(facefilter):
            brepfaces = [face for face in brep.Faces if facefilter(face)]
        else:
            brepfaces = brep.Faces
        # vertex maps and face lists
        gkey_xyz = {}
        faces = []
        for face in brepfaces:
            loop = face.OuterLoop
            curve = loop.To3dCurve()
            segments = curve.Explode()
            a = segments[0].PointAtStart
            b = segments[0].PointAtEnd
            a_gkey = geometric_key(a)
            b_gkey = geometric_key(b)
            gkey_xyz[a_gkey] = a
            gkey_xyz[b_gkey] = b
            face = [a_gkey, b_gkey]
            for segment in segments[1:-1]:
                b = segment.PointAtEnd
                b_gkey = geometric_key(b)
                face.append(b_gkey)
                gkey_xyz[b_gkey] = b
            faces.append(face)
        # vertices and faces
        gkey_index = {gkey: index for index, gkey in enumerate(gkey_xyz)}
        vertices = [list(xyz) for gkey, xyz in gkey_xyz.items()]
        faces = [[gkey_index[gkey] for gkey in face] for face in faces]
        # remove duplicates from vertexlist
        polygons = []
        for temp in faces:
            face = []
            for vertex in temp:
                if vertex not in face:
                    face.append(vertex)
            polygons.append(face)
        # define mesh type
        cls = cls or Mesh
        # create mesh
        mesh = cls.from_vertices_and_faces(vertices, polygons)
        mesh.name = self.name
        # remove isolated faces
        if cleanup:
            if mesh.number_of_faces() > 1:
                for face in list(mesh.faces()):
                    if not mesh.face_neighbors(face):
                        mesh.delete_face(face)
            mesh.remove_unused_vertices()
        return mesh

    def to_compas_quadmesh(self, nu, nv=None, weld=False, facefilter=None, cls=None):
        """Convert the surface to a COMPAS mesh.

        Parameters
        ----------
        nu: int
            The number of faces in the u direction.
        nv: int, optional
            The number of faces in the v direction.
            Default is the same as the u direction.
        weld: bool, optional
            Weld the vertices of the mesh.
            Default is False.
        facefilter: callable, optional
            A filter for selection which Brep faces to include.
            If provided, the filter should return True or False per face.
            A very simple filter that includes all faces is ``def facefilter(face): return True``.
            Default parameter value is None in which case all faces are included.
        cls: :class:`compas.geometry.Mesh`, optional
            The type of COMPAS mesh.

        Returns
        -------
        :class:`compas.geometry.Mesh`
        """
        nv = nv or nu
        cls = cls or Mesh

        if not self.geometry.HasBrepForm:
            return

        brep = Rhino.Geometry.Brep.TryConvertBrep(self.geometry)

        if facefilter and callable(facefilter):
            faces = [face for face in brep.Faces if facefilter(face)]
        else:
            faces = brep.Faces

        meshes = []
        for face in faces:
            domain_u = face.Domain(0)
            domain_v = face.Domain(1)
            du = (domain_u[1] - domain_u[0]) / (nu)
            dv = (domain_v[1] - domain_v[0]) / (nv)

            @memoize
            def point_at(i, j):
                return point_to_compas(face.PointAt(i, j))

            quads = []
            for i in range(nu):
                for j in range(nv):
                    a = point_at(domain_u[0] + (i + 0) * du, domain_v[0] + (j + 0) * dv)
                    b = point_at(domain_u[0] + (i + 1) * du, domain_v[0] + (j + 0) * dv)
                    c = point_at(domain_u[0] + (i + 1) * du, domain_v[0] + (j + 1) * dv)
                    d = point_at(domain_u[0] + (i + 0) * du, domain_v[0] + (j + 1) * dv)
                    quads.append([a, b, c, d])

            meshes.append(cls.from_polygons(quads))

        return meshes_join(meshes, cls=cls)

    def closest_point(self, xyz):
        """Return the XYZ coordinates of the closest point on the surface from input XYZ-coordinates.

        Parameters
        ----------
        xyz : list
            XYZ coordinates.

        Returns
        -------
        list
            The XYZ coordinates of the closest point on the surface.

        """
        return compas_rhino.rs.EvaluateSurface(self.guid, * compas_rhino.rs.SurfaceClosestPoint(self.guid, xyz))

    def closest_points(self, points):
        return [self.closest_point(point) for point in points]

    # def closest_point_on_boundaries(self, xyz):
    #     """Return the XYZ coordinates of the closest point on the boundaries of the surface from input XYZ-coordinates.

    #     Parameters
    #     ----------
    #     xyz : list
    #         XYZ coordinates.

    #     Returns
    #     -------
    #     list
    #         The XYZ coordinates of the closest point on the boundaries of the surface.

    #     """
    #     from compas_rhino.geometry.curve import RhinoCurve
    #     borders = self.borders(type=0)
    #     proj_dist = {tuple(proj_xyz): distance_point_point(xyz, proj_xyz) for proj_xyz in [RhinoCurve(border).closest_point(xyz) for border in borders]}
    #     compas_rhino.delete_objects(borders)
    #     return min(proj_dist, key=proj_dist.get)

    # def closest_points_on_boundaries(self, points):
    #     return [self.closest_point_on_boundaries(point) for point in points]

    # def space(self, density=(10, 10)):
    #     """Construct a parameter grid overt the UV space of the surface.

    #     Parameters
    #     ----------
    #     density : tuple, optional
    #         The density in the U and V directions of the parameter space.
    #         Default is ``10`` in both directions.

    #     Returns
    #     -------
    #     list
    #         A list of UV parameter tuples.
    #     """
    #     rs = compas_rhino.rs
    #     rs.EnableRedraw(False)
    #     try:
    #         du, dv = density
    #     except TypeError:
    #         du = density
    #         dv = density
    #     density_u = int(du)
    #     density_v = int(dv)
    #     if rs.IsPolysurface(self.guid):
    #         faces = rs.ExplodePolysurfaces(self.guid)
    #     elif rs.IsSurface(self.guid):
    #         faces = [self.guid]
    #     else:
    #         raise Exception('Object is not a (poly)surface.')
    #     uv = []
    #     for face in faces:
    #         domain_u = rs.SurfaceDomain(face, 0)
    #         domain_v = rs.SurfaceDomain(face, 1)
    #         du = (domain_u[1] - domain_u[0]) / (density_u - 1)
    #         dv = (domain_v[1] - domain_v[0]) / (density_v - 1)
    #         for i, j in product(range(density_u), range(density_v)):
    #             uv.append((domain_u[0] + i * du, domain_v[0] + j * dv))
    #     if len(faces) > 1:
    #         rs.DeleteObjects(faces)
    #     rs.EnableRedraw(True)
    #     return uv

    # def heightfield(self, density=(10, 10), over_space=True):
    #     """Construct a point grid over the surface.

    #     Parameters
    #     ----------
    #     density : tuple, optional
    #         The density in the U and V directions of the grid.
    #         Default is ``10`` in both directions.
    #     over_space : bool, optional
    #         Construct the grid over the UV space of the surface.
    #         Default is True.

    #     Returns
    #     -------
    #     list
    #         List of grid points.

    #     """
    #     rs = compas_rhino.rs
    #     rs.EnableRedraw(False)
    #     try:
    #         du, dv = density
    #     except TypeError:
    #         du = density
    #         dv = density
    #     du = int(du)
    #     dv = int(dv)
    #     if rs.IsPolysurface(self.guid):
    #         faces = rs.ExplodePolysurfaces(self.guid)
    #     elif rs.IsSurface(self.guid):
    #         faces = [self.guid]
    #     else:
    #         raise Exception('Object is not a surface.')
    #     xyz = []
    #     if over_space:
    #         for guid in faces:
    #             face = RhinoSurface.from_guid(guid)
    #             uv = face.space(density)
    #             for u, v in uv:
    #                 xyz.append(list(rs.EvaluateSurface(face.guid, u, v)))
    #     else:
    #         for guid in faces:
    #             bbox = rs.BoundingBox(guid)
    #             xmin = bbox[0][0]
    #             xmax = bbox[1][0]
    #             ymin = bbox[0][1]
    #             ymax = bbox[3][1]
    #             xstep = 1.0 * (xmax - xmin) / (du - 1)
    #             ystep = 1.0 * (ymax - ymin) / (dv - 1)
    #             seeds = []
    #             for i in range(du):
    #                 for j in range(dv):
    #                     seed = xmin + i * xstep, ymin + j * ystep, 0
    #                     seeds.append(seed)
    #             points = map(list, rs.ProjectPointToSurface(seeds, guid, [0, 0, 1]))
    #             xyz += points
    #     if len(faces) > 1:
    #         rs.DeleteObjects(faces)
    #     rs.EnableRedraw(True)
    #     return xyz

    # def descent(self, points=None):
    #     """"""
    #     rs = compas_rhino.rs
    #     if not points:
    #         points = self.heightfield()
    #     tol = rs.UnitAbsoluteTolerance()
    #     descent = []
    #     if rs.IsPolysurface(self.guid):
    #         rs.EnableRedraw(False)
    #         faces = {}
    #         for p0 in points:
    #             p = p0[:]
    #             p[2] -= 2 * tol
    #             bcp = rs.BrepClosestPoint(self.guid, p)
    #             uv = bcp[1]
    #             index = bcp[2][1]
    #             try:
    #                 face = faces[index]
    #             except (TypeError, IndexError):
    #                 face = rs.ExtractSurface(self.guid, index, True)
    #                 faces[index] = face
    #             p1 = rs.EvaluateSurface(face, uv[0], uv[1])
    #             vector = [p1[_] - p0[_] for _ in range(3)]
    #             descent.append((p0, vector))
    #         rs.DeleteObjects(faces.values())
    #         rs.EnableRedraw(True)
    #     elif rs.IsSurface(self.guid):
    #         for p0 in points:
    #             p = p0[:]
    #             p[2] -= 2 * tol
    #             bcp = rs.BrepClosestPoint(self.guid, p)
    #             uv = bcp[1]
    #             p1 = rs.EvaluateSurface(self.guid, uv[0], uv[1])
    #             vector = [p1[_] - p0[_] for _ in range(3)]
    #             descent.append((p0, vector))
    #     else:
    #         raise Exception('Object is not a surface.')
    #     return descent

    # def curvature(self, points=None):
    #     """"""
    #     rs = compas_rhino.rs
    #     if not points:
    #         points = self.heightfield()
    #     curvature = []
    #     if rs.IsPolysurface(self.guid):
    #         rs.EnableRedraw(False)
    #         faces = {}
    #         for point in points:
    #             bcp = rs.BrepClosestPoint(self.guid, point)
    #             uv = bcp[1]
    #             index = bcp[2][1]
    #             try:
    #                 face = faces[index]
    #             except (TypeError, IndexError):
    #                 face = rs.ExtractSurface(self.guid, index, True)
    #                 faces[index] = face
    #             props = rs.SurfaceCurvature(face, uv)
    #             curvature.append((point, (props[1], props[3], props[5])))
    #         rs.DeleteObjects(faces.values())
    #         rs.EnableRedraw(False)
    #     elif rs.IsSurface(self.guid):
    #         for point in points:
    #             bcp = rs.BrepClosestPoint(self.guid, point)
    #             uv = bcp[1]
    #             props = rs.SurfaceCurvature(self.guid, uv)
    #             curvature.append((point, (props[1], props[3], props[5])))
    #     else:
    #         raise Exception('Object is not a surface.')
    #     return curvature

    # def borders(self, border_type=1):
    #     """Duplicate the borders of the surface.

    #     Parameters
    #     ----------
    #     border_type : {0, 1, 2}
    #         The type of border.

    #         * 0: All borders
    #         * 1: The exterior borders.
    #         * 2: The interior borders.

    #     Returns
    #     -------
    #     list
    #         The GUIDs of the extracted border curves.
    #     """
    #     rs = compas_rhino.rs
    #     border = rs.DuplicateSurfaceBorder(self.guid, type=border_type)
    #     curves = rs.ExplodeCurves(border, delete_input=True)
    #     return curves

    # def kinks(self, threshold=1e-3):
    #     """Return the XYZ coordinates of kinks, i.e. tangency discontinuities, along the surface's boundaries.

    #     Returns
    #     -------
    #     list
    #         The list of XYZ coordinates of surface boundary kinks.
    #     """
    #     from compas_rhino.geometry.curve import RhinoCurve
    #     rs = compas_rhino.rs
    #     kinks = []
    #     borders = self.borders(border_type=0)
    #     for border in borders:
    #         border = RhinoCurve(border)
    #         extremities = map(lambda x: rs.EvaluateCurve(border.guid, rs.CurveParameter(border.guid, x)), [0., 1.])
    #         if border.is_closed():
    #             start_tgt, end_tgt = border.tangents(extremities)
    #             if angle_vectors(start_tgt, end_tgt) > threshold:
    #                 kinks += extremities
    #         else:
    #             kinks += extremities
    #     return list(set(kinks))


# src/compas_rhino/conversions/vector.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import Rhino

from ._primitives import vector_to_rhino
from ._primitives import vector_to_compas

from ._geometry import RhinoGeometry


class RhinoVector(RhinoGeometry):
    """Wrapper for Rhino vectors."""

    @property
    def geometry(self):
        return self._geometry

    @geometry.setter
    def geometry(self, geometry):
        """Set the geometry of the wrapper.

        Parameters
        ----------
        geometry : :rhino:`Rhino_Geometry_Vector3d` or :class:`compas.geometry.Vector` or list of float
            The input geometry.

        Raises
        ------
        :class:`ConversionError`
            If the geometry cannot be converted to a vector.
        """
        if not isinstance(geometry, Rhino.Geometry.Vector3d):
            geometry = vector_to_rhino(geometry)
        self._geometry = geometry

    def to_compas(self):
        """Convert the wrapper to a COMPAS object.

        Returns
        -------
        :class:`compas.geometry.Vector`
            A COMPAS vector.
        """
        return vector_to_compas(self.geometry)


# src/compas_rhino/forms/__init__.py

"""
********************************************************************************
forms
********************************************************************************

.. currentmodule:: compas_rhino.forms


Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    BrowserForm
    ChartForm
    ImageForm
    SliderForm
    TextForm


Base Classes
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    BaseForm


"""
from __future__ import absolute_import

from .base import BaseForm

from .browser import BrowserForm
from .chart import ChartForm
from .slider import SliderForm

try:
    from .etoforms import ImageForm
    from .etoforms import TextForm
except Exception:
    from .image import ImageForm
    from .text import TextForm


__all__ = [
    'BaseForm',
    'BrowserForm',
    'ChartForm',
    'SliderForm',
    'ImageForm',
    'TextForm'
]

try:
    from .etoforms import PropertyListForm
    from .etoforms import SettingsForm
except Exception:
    pass
else:
    __all__ += [
        'PropertyListForm',
        'SettingsForm'
    ]


# src/compas_rhino/forms/base.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import System
from System.Windows.Forms import DialogResult
from System.Windows.Forms import FormBorderStyle

import Rhino


class BaseForm(System.Windows.Forms.Form):
    """Base class for Windows forms.

    Parameters
    ----------
    title : str, optional
        The title of the form.
    width : int, optional
        The width of the form.
    height : int, optional
        The height of the form.

    """

    def __init__(self, title='Form', width=None, height=None):
        self.Text = title
        if width:
            self.Width = width
        if height:
            self.Height = height
        self.MaximizeBox = False
        self.FormBorderStyle = FormBorderStyle.FixedSingle
        self.SuspendLayout()
        self.init()
        self.ResumeLayout()
        self.FormClosed += self.on_form_closed

    def init(self):
        """Initialize the form.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def show(self):
        """Show the form as a modal dialog.

        Returns
        -------
        bool
            True if the dialog was closed using the OK button.
            False otherwise.

        """
        if Rhino.UI.Dialogs.ShowSemiModal(self) == DialogResult.OK:
            return True
        return False

    def on_form_closed(self, sender, eargs):
        """Callback for the closing event of the form.

        Parameters
        ----------
        sender : System.Object
            The sender object.
        eargs : System.Object.EventArgs
            The event arguments.

        Returns
        -------
        None

        """
        pass


# src/compas_rhino/forms/browser.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

try:
    basestring
except NameError:
    basestring = str

from compas_rhino.forms.base import BaseForm

from System import Uri
from System.Windows.Forms import WebBrowser
from System.Windows.Forms import StatusStrip
from System.Windows.Forms import ToolStripStatusLabel
from System.Windows.Forms import FormBorderStyle
from System.Windows.Forms import DockStyle


class BrowserForm(BaseForm):
    """A form for displaying web pages.

    Parameters
    ----------
    url : str
        The url of a web page.
    title : str, optional
        The title of the form.
    width : int, optional
        The width of the form.
    height : int, optional
        The height of the form.

    Attributes
    ----------
    url : System.Uri
        Address of the resource to display in the browser.

    Examples
    --------
    .. code-block:: python

        form = BrowserForm('http://block.arch.ethz.ch')
        form.show()

    """

    def __init__(self, url, title='Browser', width=1024, height=786):
        self._url = None
        self.url = url
        self.FormBorderStyle = FormBorderStyle.Sizable
        super(BrowserForm, self).__init__(title, width, height)

    @property
    def url(self):
        return self._url

    @url.setter
    def url(self, url):
        if isinstance(url, Uri):
            self._url = url
        elif isinstance(url, basestring):
            self._url = Uri(url)
        else:
            raise NotImplementedError

    def init(self):
        """Initialize the form.

        Returns
        -------
        None

        """
        self.browser = WebBrowser()
        self.browser.Url = self.url
        self.browser.StatusTextChanged += self.on_statustext_changed
        self.browser.Dock = DockStyle.Fill
        self.status_strip = StatusStrip()
        self.status = ToolStripStatusLabel()
        self.status_strip.Items.Add(self.status)
        self.Controls.Add(self.browser)
        self.Controls.Add(self.status_strip)

    def on_statustext_changed(self, sender, eargs):
        """Callback for changes in the status text.

        Parameters
        ----------
        sender : System.Object
            The sender object.
        eargs : System.Object.EventArgs
            The event arguments.

        Returns
        -------
        None

        """
        self.status.Text = self.browser.StatusText


# src/compas_rhino/forms/chart.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

try:
    basestring
except NameError:
    basestring = str

from compas_rhino.forms.base import BaseForm

import clr
from System.Drawing import Size
from System.Drawing import Point
from System.Drawing import Color

clr.AddReference("System.Windows.Forms.DataVisualization")
from System.Windows.Forms.DataVisualization import Charting  # noqa: E402


class Series(object):
    pass


class ChartForm(BaseForm):
    """A form for displaying charts.

    Parameters
    ----------
    series : list[dict]
        A list of dictionaries with each dictionary defining the attributes of a series.
        The following attributes are supported:

        * name: The name of the series.
        * data: A dictionary with x-y pairs.
        * color (optional): A hex color or an RGB(255) color specification.
        * linewidth (optional): The width of the series graph line.
        * linetype (optional): The visual style of the graph line.
          Should be one of ``{'solid', 'dotted', 'dashed'}``.

    xlimits : tuple[float, float]
        Minimum and maximum values on the X-axis.
    xstep : int
        Size of the steps along the X-axis.
    ylimits : tuple[float, float], optional
        Minimum and maximum values on the Y-axis.
        Default is None, in which case the limits will be computed from the
        min/max values of the data in the series.
    ystep : int, optional
        Size of the steps along the Y-axis.
        Default is ``int((ymax - ymin) / 10.)```.
    chartsize : tuple[int, int], optional
        The size of the form in pixels.
    padding : tuple[int, int, int, int], optional
        Padding of the area inside the form.
    bgcolor : tuple[int, int, int], optional
        The background color of the chart area.
    title : str, optional
        Title of the form.

    Examples
    --------
    .. code-block:: python

        import random

        from compas.utilities import fibonacci
        from compas_rhino.forms import ChartForm

        series = [
            {
                'name'      : 'series1',
                'color'     : (255, 0, 0),
                'linewidth' : 1,
                'linestyle' : 'dashed',
                'data'      : dict((str(i), random.randint(30, 70)) for i in range(10)),
            },
            {
                'name'      : 'series2',
                'color'     : (0, 255, 0),
                'linewidth' : 1,
                'linestyle' : 'solid',
                'data'      : dict((str(i), i ** 2) for i in range(10)),
            },
            {
                'name'      : 'series3',
                'color'     : (0, 0, 255),
                'linewidth' : 1,
                'linestyle' : 'dotted',
                'data'      : dict((str(i), fibonacci(i)) for i in range(10)),
            },
        ]

        form = ChartForm(series, (0, 10), 1)
        form.show()

    """

    def __init__(self, series,
                 xlimits, xstep,
                 ylimits=None, ystep=None,
                 chartsize=(800, 600), padding=(20, 20, 20, 20),
                 bgcolor=None,
                 title='Chart', **kwargs):

        self._bgcolor = None

        w, h = chartsize
        self.chartwidth = w
        self.chartheight = h
        self.padding = padding
        self.series = series
        self.xmin = xlimits[0]
        self.xmax = xlimits[1]
        self.xstep = xstep
        self.bgcolor = bgcolor

        self.ymin = 0
        self.ymax = 0
        self.ystep = None

        for attr in series:
            keys = sorted(attr['data'].keys(), key=int)
            values = [attr['data'][key] for key in keys]
            y = map(float, values)
            self.ymin = min(min(y), self.ymin)
            self.ymax = max(max(y), self.ymax)

        self.ystep = int((self.ymax - self.ymin) / 10.)

        super(ChartForm, self).__init__(title)

    @property
    def bgcolor(self):
        return self._bgcolor

    @bgcolor.setter
    def bgcolor(self, colour):
        if not colour:
            self._bgcolor = Color.White
        elif isinstance(colour, Color):
            self._bgcolor = colour
        elif isinstance(colour, basestring):
            raise NotImplementedError
        elif isinstance(colour, tuple):
            self._bgcolor = Color.FromArgb(* colour)
        else:
            raise NotImplementedError

    def init(self):
        """Initialize the form.

        Returns
        -------
        None

        """
        w = self.chartwidth + self.padding[1] + self.padding[3]
        h = self.chartheight + self.padding[0] + self.padding[2]
        self.ClientSize = Size(w, h)

        charting = Charting
        chart = charting.Chart()
        chart.Location = Point(self.padding[3], self.padding[0])
        chart.Size = Size(self.chartwidth, self.chartheight)
        chart.ChartAreas.Add('series')
        area = chart.ChartAreas['series']

        x = area.AxisX
        x.Minimum = self.xmin
        x.Maximum = self.xmax
        x.Interval = self.xstep
        x.MajorGrid.LineColor = Color.White
        x.MajorGrid.LineDashStyle = charting.ChartDashStyle.NotSet

        y = area.AxisY
        y.Minimum = self.ymin
        y.Maximum = self.ymax
        y.Interval = self.ystep
        y.MajorGrid.LineColor = Color.Gray
        y.MajorGrid.LineDashStyle = charting.ChartDashStyle.Dot

        for attr in self.series:
            name = attr['name']
            color = attr['color']
            linewidth = attr['linewidth']
            chart.Series.Add(name)
            series = chart.Series[name]
            series.ChartType = charting.SeriesChartType.Line
            series.Color = Color.FromArgb(*color)
            series.BorderWidth = linewidth
            keys = sorted(attr['data'].keys(), key=int)
            for key in keys:
                value = attr['data'][key]
                series.Points.AddXY(int(key), value)

        area.BackColor = self.bgcolor

        self.Controls.Add(chart)


# src/compas_rhino/forms/image.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

try:
    basestring
except NameError:
    basestring = str

from compas_rhino.forms.base import BaseForm

from System.Windows.Forms import PictureBox
from System.Windows.Forms import PictureBoxSizeMode
from System.Windows.Forms import DockStyle
from System.Drawing import Image
from System.Net import WebClient
from System.IO import MemoryStream


__all__ = ['ImageForm', 'image_from_remote', 'image_from_local']


def image_from_remote(source):
    """Construct an image from a remote source.

    Parameters
    ----------
    source : str
        The url of the remote source.

    Returns
    -------
    System.Drawing.Image
        Representation of an image in memory.

    Examples
    --------
    .. code-block:: python

        image = image_from_remote('http://block.arch.ethz.ch/brg/images/cache/dsc02360_ni-2_cropped_1528706473_624x351.jpg')

    """
    w = WebClient()
    d = w.DownloadData(source)
    m = MemoryStream(d)
    return Image.FromStream(m)


def image_from_local(source):
    """Construct an image from a local source.

    Parameters
    ----------
    source : str
        The path to the local source file.

    Returns
    -------
    System.Drawing.Image
        Representation of an image in memory.

    Examples
    --------
    .. code-block:: python

        image = image_from_local('theblock.jpg')

    """
    return Image.FromFile(source)


class ImageForm(BaseForm):
    """A form for displaying images.

    Parameters
    ----------
    image : str or Image
        The image that should be displayed.
        This can be a url of a remote image file,
        or a local file path,
        or an instance of `System.Drawing.Image`.
    title : str, optional
        Title of the form.
    width : int, optional
        Width of the form.
    height : int, optional
        Height of the form.

    Attributes
    ----------
    image : System.Drawing.Image
        The image displayed by the form.

    Examples
    --------
    .. code-block:: python

        from compas_rhino.forms import ImageForm

        form = ImageForm('http://block.arch.ethz.ch/brg/images/cache/dsc02360_ni-2_cropped_1528706473_624x351.jpg')
        form.show()

    """

    def __init__(self, image, title='Image', width=None, height=None):
        self._image = None
        self.image = image
        super(ImageForm, self).__init__(title, width, height)

    @property
    def image(self):
        return self._image

    @image.setter
    def image(self, image):
        if isinstance(image, basestring):
            if image.startswith('http'):
                self._image = image_from_remote(image)
            else:
                self._image = image_from_local(image)
        elif isinstance(image, Image):
            self._image = image
        else:
            raise NotImplementedError

    def init(self):
        """Initialize the form.

        Returns
        -------
        None

        """
        box = PictureBox()
        box.Dock = DockStyle.Fill
        box.SizeMode = PictureBoxSizeMode.AutoSize
        box.Image = self.image
        self.image = box.Image
        self.Controls.Add(box)
        self.ClientSize = box.Size

    def on_form_closed(self, sender, e):
        """Callback for the closing event of the form.

        Parameters
        ----------
        sender : System.Object
            The sender object.
        eargs : System.Object.EventArgs
            The event arguments.

        Returns
        -------
        None

        """
        self.image.Dispose()


# src/compas_rhino/forms/slider.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas_rhino.forms.base import BaseForm

import System
from System.Drawing import Size
from System.Drawing import Point
from System.Drawing import Color
from System.Windows.Forms import TextBox
from System.Windows.Forms import TrackBar

import scriptcontext as sc


__all__ = ['SliderForm']


class SliderForm(BaseForm):
    """A form for sliders.

    Parameters
    ----------
    minval : int
        The lowest value of the sliding range.
    maxval : int
        The highest value of the sliding range.
    step : int
        Size of one increment in the sliding range.
    value : int
        Starting value.

    """

    def __init__(self, minval, maxval, step, value):
        self.minval = minval
        self.maxval = maxval
        self.step = step
        self.value = value
        super(SliderForm, self).__init__()

    def init(self):
        """Initialize the form.

        Returns
        -------
        None

        """
        textbox = TextBox()
        textbox.Text = str(self.value)
        textbox.Location = Point(10, 10)
        textbox.Width = 40
        textbox.TextChanged += System.EventHandler(self.on_textchanged)
        trackbar = TrackBar()
        trackbar.Minimum = self.minval
        trackbar.Maximum = self.maxval
        trackbar.SmallChange = self.step
        trackbar.LargeChange = self.step
        trackbar.TickFrequency = self.step
        trackbar.Value = self.value
        trackbar.Width = 460
        trackbar.Location = Point(60, 10)
        trackbar.Scroll += System.EventHandler(self.on_scroll)
        self.Controls.Add(textbox)
        self.Controls.Add(trackbar)
        self.ClientSize = Size(10 + textbox.Width + 10 + trackbar.Width + 10, trackbar.Height + 10)
        self.textbox = textbox
        self.trackbar = trackbar

    def on_textchanged(self, sender, e):
        """Callback for changes made to the text input field.

        Parameters
        ----------
        sender : System.Object
            The sender object.
        e : System.Object.EventArgs
            The event arguments.

        Returns
        -------
        None

        """
        if sender.Text:
            self.trackbar.Value = int(sender.Text)

    def on_scroll(self, sender, e):
        """Callback for changes made with the slider.

        Parameters
        ----------
        sender : System.Object
            The sender object.
        e : System.Object.EventArgs
            The event arguments.

        Returns
        -------
        None

        """
        self.textbox.Text = str(sender.Value)
        sc.doc.Views.Redraw()


# ==============================================================================
# Main
# ==============================================================================

if __name__ == '__main__':

    import Rhino
    from Rhino.Geometry import Point3d
    from Rhino.Geometry import Plane
    from Rhino.Geometry import Circle
    from Rhino.Geometry import Cylinder

    from Rhino.Display import DisplayMaterial

    class Pipe(Rhino.Display.DisplayConduit):
        """"""

        def __init__(self, slider):
            super(Pipe, self).__init__()
            self.slider = slider
            self.base = Point3d(0, 0, 0)
            self.normal = Point3d(0, 0, 1) - self.base
            self.height = 30
            self.plane = Plane(self.base, self.normal)
            self.color = Color.FromArgb(255, 0, 0)
            self.material = DisplayMaterial(self.color)

        def DrawForeground(self, e):
            radius = self.slider.trackbar.Value
            circle = Circle(self.plane, radius)
            cylinder = Cylinder(circle, self.height)
            brep = cylinder.ToBrep(True, True)
            e.Display.DrawBrepShaded(brep, self.material)

    try:
        slider = SliderForm(0, 10, 1, 3)
        pipe = Pipe(slider)
        pipe.Enabled = True
        sc.doc.Views.Redraw()
        slider.show()

    except Exception as e:
        print(e)

    finally:
        pipe.Enabled = False
        del pipe


# src/compas_rhino/forms/text.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas_rhino.forms.base import BaseForm

from System.Windows.Forms import TextBox
from System.Windows.Forms import DockStyle
from System.Windows.Forms import ScrollBars
from System.Drawing import Font
from System.Drawing import FontFamily
from System.Environment import NewLine


__all__ = ['TextForm']


class TextForm(BaseForm):
    """A form for text.

    Parameters
    ----------
    text : str
        The text to display.
    title : str, optional
        Title of the form.
    width : int, optional
        Width of the form.
    height : int, optional
        Height of the form.

    """

    def __init__(self, text, title='Message', width=800, height=600):
        self.text = text
        super(TextForm, self).__init__(title, width, height)

    def init(self):
        """Initialize the form.

        Returns
        -------
        None

        """
        textbox = TextBox()
        textbox.ReadOnly = True
        textbox.Dock = DockStyle.Fill
        textbox.Multiline = True
        textbox.ScrollBars = ScrollBars.Vertical
        textbox.Font = Font(FontFamily.GenericMonospace, 8.0)
        if isinstance(self.text, str):
            textbox.Text = self.text
        else:
            try:
                textbox.Text = (NewLine).join(self.text)
            except Exception as e:
                textbox.Text = str(e)
        self.Controls.Add(textbox)


# src/compas_rhino/objects/__init__.py

"""
********************************************************************************
objects
********************************************************************************

.. currentmodule:: compas_rhino.objects

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    MeshObject
    NetworkObject
    VolMeshObject

"""
from __future__ import absolute_import

from ._select import (  # noqa : F401 F403
    mesh_select_vertex,
    mesh_select_vertices,
    mesh_select_face,
    mesh_select_faces,
    mesh_select_edge,
    mesh_select_edges,
    network_select_node,
    network_select_nodes,
    network_select_edge,
    network_select_edges
)
from ._modify import (  # noqa : F401 F403
    network_update_attributes,
    network_update_node_attributes,
    network_update_edge_attributes,
    network_move_node,
    mesh_update_attributes,
    mesh_update_vertex_attributes,
    mesh_update_face_attributes,
    mesh_update_edge_attributes,
    mesh_move_vertex,
    mesh_move_vertices,
    mesh_move_face
)

from ._object import BaseObject
from .meshobject import MeshObject
from .networkobject import NetworkObject
from .volmeshobject import VolMeshObject

from compas.datastructures import Mesh
from compas.datastructures import Network
from compas.datastructures import VolMesh

# import warnings

# warnings.warn(
#     "The objects module in compas_rhino is deprecated. Use the equivalent functionality from compas_ui instead",
#     DeprecationWarning,
#     stacklevel=2
# )

BaseObject.register(Mesh, MeshObject)
BaseObject.register(Network, NetworkObject)
BaseObject.register(VolMesh, VolMeshObject)


__all__ = [
    'BaseObject',
    'MeshObject',
    'NetworkObject',
    'VolMeshObject'
]


# src/compas_rhino/objects/_modify.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import ast
import compas_rhino

from compas.geometry import add_vectors

import Rhino
import clr

clr.AddReference('Rhino.UI')
import Rhino.UI  # noqa: E402
from Rhino.Geometry import Point3d  # noqa: E402

try:
    from compas_rhino.forms import PropertyListForm
except ImportError:
    from Rhino.UI.Dialogs import ShowPropertyListBox


__all__ = [
    'network_update_attributes',
    'network_update_node_attributes',
    'network_update_edge_attributes',

    'network_move_node',

    'mesh_update_attributes',
    'mesh_update_vertex_attributes',
    'mesh_update_face_attributes',
    'mesh_update_edge_attributes',

    'mesh_move_vertex',
    'mesh_move_vertices',
    'mesh_move_face',
]


def _update_named_values(names, values, message='', title='Update named values'):
    try:
        dialog = PropertyListForm(names, values)
    except Exception:
        values = ShowPropertyListBox(message, title, names, values)
    else:
        if dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow):
            values = dialog.values
        else:
            values = None
    return values


def network_update_attributes(network):
    """Update the attributes of a network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.
    """
    names = sorted(network.attributes.keys())
    values = [str(network.attributes[name]) for name in names]
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            try:
                network.attributes[name] = ast.literal_eval(value)
            except (ValueError, TypeError):
                network.attributes[name] = value
        return True
    return False


def network_update_node_attributes(network, nodes, names=None):
    """Update the attributes of the nodes of a network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.
    nodes : list
        The identifiers of the nodes to update.
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or network.default_node_attributes.keys()
    names = sorted(names)
    values = network.node_attributes(nodes[0], names)
    if len(nodes) > 1:
        for i, name in enumerate(names):
            for node in nodes[1:]:
                if values[i] != network.node_attribute(node, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for node in nodes:
                try:
                    network.node_attribute(node, name, ast.literal_eval(value))
                except (ValueError, TypeError):
                    network.node_attribute(node, name, value)
        return True
    return False


def network_update_edge_attributes(network, edges, names=None):
    """Update the attributes of the edges of a network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.
    edges : list
        The identifiers of the edges to update.
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or network.default_edge_attributes.keys()
    names = sorted(names)
    edge = edges[0]
    values = network.edge_attributes(edge, names)
    if len(edges) > 1:
        for i, name in enumerate(names):
            for edge in edges[1:]:
                if values[i] != network.edge_attribute(edge, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for edge in edges:
                try:
                    value = ast.literal_eval(value)
                except (SyntaxError, ValueError, TypeError):
                    pass
                network.edge_attribute(edge, name, value)
        return True
    return False


def network_move_node(network, node, constraint=None, allow_off=False):
    """Move on node of the network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.
    node : hashable
        The identifier of the node to move.
    constraint : Rhino.Geometry, optional
        A Rhino geometry object to constrain the movement to.
        By default the movement is unconstrained.
    allow_off : bool, optional (False)
        Allow the node to move off the constraint.

    """
    def OnDynamicDraw(sender, e):
        for ep in nbrs:
            sp = e.CurrentPoint
            e.Display.DrawDottedLine(sp, ep, color)

    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    nbrs = [network.node_coordinates(nbr) for nbr in network.node_neighbors(node)]
    nbrs = [Point3d(*xyz) for xyz in nbrs]

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move to?')
    gp.DynamicDraw += OnDynamicDraw
    if constraint:
        gp.Constrain(constraint, allow_off)

    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    network.node_attributes(node, 'xyz', list(gp.Point()))
    return True


def mesh_update_attributes(mesh):
    """Update the attributes of a mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A mesh object.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = sorted(mesh.attributes.keys())
    values = [str(mesh.attributes[name]) for name in names]
    values = compas_rhino.update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            try:
                mesh.attributes[name] = ast.literal_eval(value)
            except (ValueError, TypeError):
                mesh.attributes[name] = value
        return True
    return False


def mesh_update_vertex_attributes(mesh, vertices, names=None):
    """Update the attributes of selected vertices of a given datastructure.

    Parameters
    ----------
    mesh : compas.datastructures.Datastructure
        The data structure.
    vertices : list
        The vertices of the vertices of which the attributes should be updated.
    names : list, optional
        The names of the attributes that should be updated.
        Default is to update all available attributes.

    Returns
    -------
    bool
        True if the attributes were successfully updated.
        False otherwise.

    """
    names = names or mesh.default_vertex_attributes.keys()
    names = sorted(names)
    values = mesh.vertex_attributes(vertices[0], names)
    if len(vertices) > 1:
        for i, name in enumerate(names):
            for vertex in vertices[1:]:
                if values[i] != mesh.vertex_attribute(vertex, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for vertex in vertices:
                try:
                    mesh.vertex_attribute(vertex, name, ast.literal_eval(value))
                except (ValueError, TypeError):
                    mesh.vertex_attribute(vertex, name, value)
        return True
    return False


# rename to modify
def mesh_update_face_attributes(mesh, faces, names=None):
    """Update the attributes of the faces of a mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
    faces : list of int
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or mesh.default_face_attributes.keys()
    names = sorted(names)
    values = mesh.face_attributes(faces[0], names)
    if len(faces) > 1:
        for i, name in enumerate(names):
            for face in faces[1:]:
                if values[i] != mesh.face_attribute(face, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for face in faces:
                try:
                    mesh.face_attribute(face, name, ast.literal_eval(value))
                except (ValueError, TypeError):
                    mesh.face_attribute(face, name, value)
        return True
    return False


def mesh_update_edge_attributes(mesh, edges, names=None):
    """Update the attributes of the edges of a mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A mesh object.
    edges : list
        The edges to update.
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or mesh.default_edge_attributes.keys()
    names = sorted(names)
    edge = edges[0]
    values = mesh.edge_attributes(edge, names)
    if len(edges) > 1:
        for i, name in enumerate(names):
            for edge in edges[1:]:
                if values[i] != mesh.edge_attribute(edge, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for edge in edges:
                try:
                    value = ast.literal_eval(value)
                except (SyntaxError, ValueError, TypeError):
                    pass
                mesh.edge_attribute(edge, name, value)
        return True
    return False


# def mesh_move(mesh):
#     """"""
#     color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor

#     vertex_xyz0 = {key: mesh.vertex_coordinates(key) for key in mesh.mesh.vertices()}
#     vertex_xyz = {key: mesh.vertex_coordinates(key) for key in mesh.mesh.vertices()}

#     edges = list(mesh.edges())

#     start = compas_rhino.pick_point('Point to move from?')
#     if not start:
#         return False

#     def OnDynamicDraw(sender, e):
#         current = list(e.CurrentPoint)
#         vector = subtract_vectors(current, start)
#         for vertex in vertex_xyz:
#             vertex_xyz[vertex] = add_vectors(vertex_xyz0[vertex], vector)
#         for u, v in iter(edges):
#             sp = vertex[u]
#             ep = vertex[v]
#             sp = Point3d(*sp)
#             ep = Point3d(*ep)
#             e.Display.DrawDottedLine(sp, ep, color)

#     gp = Rhino.Input.Custom.GetPoint()
#     gp.SetCommandPrompt('Point to move to?')
#     gp.DynamicDraw += OnDynamicDraw
#     gp.Get()

#     if gp.CommandResult() == Rhino.Commands.Result.Success:
#         end = list(gp.Point())
#         vector = subtract_vectors(end, start)
#         for vertex, attr in mesh.vertices(True):
#             attr['x'] += vector[0]
#             attr['y'] += vector[1]
#             attr['z'] += vector[2]
#         return True
#     return False


def mesh_move_vertex(mesh, vertex, constraint=None, allow_off=True):
    """Move on vertex of the mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
    vertex : int
    constraint : :rhino:`Rhino.Geometry`, optional
        A Rhino geometry object to constrain the movement to.
        By default the movement is unconstrained.
    allow_off : bool, optional (True)
        Allow the vertex to move off the constraint.

    """
    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    nbrs = [mesh.vertex_coordinates(nbr) for nbr in mesh.vertex_neighbors(vertex)]
    nbrs = [Point3d(*xyz) for xyz in nbrs]

    def OnDynamicDraw(sender, e):
        for ep in nbrs:
            sp = e.CurrentPoint
            e.Display.DrawDottedLine(sp, ep, color)

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move to?')
    gp.DynamicDraw += OnDynamicDraw
    if constraint:
        gp.Constrain(constraint, allow_off)

    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    mesh.vertex_attributes(vertex, 'xyz', list(gp.Point()))
    return True


def mesh_move_vertices(mesh, vertices):
    """Move on vertices of the mesh.

    Parameters
    ----------
    mesh : compas.datastructures.Mesh
        A mesh object.
    keys : list
        The vertices to move.
    constraint : Rhino.Geometry (None)
        A Rhino geometry object to constrain the movement to.
        By default the movement is unconstrained.
    allow_off : bool (False)
        Allow the vertex to move off the constraint.

    """
    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    lines = []
    connectors = []

    for vertex in vertices:
        a = mesh.vertex_coordinates(vertex)
        nbrs = mesh.vertex_neighbors(vertex)
        for nbr in nbrs:
            b = mesh.vertex_coordinates(nbr)
            line = [Point3d(* a), Point3d(* b)]
            if nbr in vertices:
                lines.append(line)
            else:
                connectors.append(line)

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move from?')
    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    start = gp.Point()

    def OnDynamicDraw(sender, e):
        end = e.CurrentPoint
        vector = end - start
        for a, b in lines:
            a = a + vector
            b = b + vector
            e.Display.DrawDottedLine(a, b, color)
        for a, b in connectors:
            a = a + vector
            e.Display.DrawDottedLine(a, b, color)

    gp.SetCommandPrompt('Point to move to?')
    gp.SetBasePoint(start, False)
    gp.DrawLineFromPoint(start, True)
    gp.DynamicDraw += OnDynamicDraw
    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    end = gp.Point()
    vector = list(end - start)

    for vertex in vertices:
        xyz = mesh.vertex_attributes(vertex, 'xyz')
        mesh.vertex_attributes(vertex, 'xyz', add_vectors(xyz, vector))
    return True


def mesh_move_face(mesh, face, constraint=None, allow_off=True):
    """Move a face of the mesh to a different location and update the data structure.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
    face : int

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    def OnDynamicDraw(sender, e):
        for ep in nbrs:
            sp = e.CurrentPoint
            e.Display.DrawDottedLine(sp, ep, color)

    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    nbrs = [mesh.face_coordinates(nbr) for nbr in mesh.face_neighbors(face)]
    nbrs = [Point3d(*xyz) for xyz in nbrs]

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move to?')
    gp.DynamicDraw += OnDynamicDraw
    if constraint:
        gp.Constrain(constraint, allow_off)

    gp.Get()

    if gp.CommandResult() == Rhino.Commands.Result.Success:
        mesh.face_attributes(face, 'xyz', list(gp.Point()))
        return True

    return False


# src/compas_rhino/objects/_object.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from uuid import uuid4
from compas_rhino.artists import RhinoArtist


_ITEM_OBJECT = {}


class BaseObject(object):
    """Abstract base class for COMPAS Rhino objects.

    Parameters
    ----------
    item : {:class:`compas.geometry.Geometry`, :class:`compas.datastructures.Datastructure`}
        A COMPAS geometry object or data structure.
    scene : :class:`compas.scenes.Scene`, optional
        A scene object.
    name : str, optional
        The name of the object.
    layer : str, optional
        The layer for drawing.
    visible : bool, optional
        Toggle for the visibility of the object.
    settings : dict, optional
        A dictionary of settings.

    Attributes
    ----------
    item : {:class:`compas.geometry.Geometry`, :class:`compas.datastructures.Datastructure`}
        A COMPAS geometry object or data structure.
    scene : :class:`compas.scenes.Scene`
        A scene object.
    artist : :class:`compas_rhino.artists.Artist`
        The artist matching the type of ``item``.
    name : str
        The name of the object.
        This is an alias for the name of ``item``.
    layer : str
        The layer for drawing.
        This is an alias for the layer of ``artist``.
    visible : bool
        Toggle for the visibility of the object in the scene.
    settings : dict
        A dictionary of settings related to visualisation and interaction.
        This dict starts from the settings of the ``artist``.

    """

    def __init__(self, item, scene=None, name=None, layer=None, visible=True, settings=None):
        super(BaseObject, self).__init__()
        self._item = None
        self._id = None
        self._scene = None
        self._artist = None
        self.scene = scene
        self.item = item
        self.name = name
        self.layer = layer
        self.visible = visible
        self.settings = settings or {}

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def scene(self):
        return self._scene

    @scene.setter
    def scene(self, scene):
        self._scene = scene

    @property
    def item(self):
        return self._item

    @item.setter
    def item(self, item):
        self._item = item
        self._artist = RhinoArtist(item)

    @property
    def artist(self):
        return self._artist

    @property
    def id(self):
        if not self._id:
            self._id = uuid4()
        return self._id

    # this is debatable
    @property
    def name(self):
        return self.item.name

    @name.setter
    def name(self, name):
        self.item.name = name

    @property
    def layer(self):
        return self.artist.layer

    @layer.setter
    def layer(self, layer):
        self.artist.layer = layer

    # ==========================================================================
    # Methods
    # ==========================================================================

    @staticmethod
    def register(item_type, object_type):
        _ITEM_OBJECT[item_type] = object_type

    @staticmethod
    def registered_object_types():
        return [_ITEM_OBJECT[item_type] for item_type in _ITEM_OBJECT]

    @staticmethod
    def build(item, **kwargs):
        object_type = _ITEM_OBJECT[type(item)]
        return object_type(item, **kwargs)

    def clear(self):
        """Clear all previously created Rhino objects."""
        raise NotImplementedError

    def clear_layer(self):
        """Clear the layer of the object."""
        self.artist.clear_layer()

    def draw(self):
        """Draw the object representing the item."""
        raise NotImplementedError

    def redraw(self):
        """Redraw the Rhino scene/view."""
        self.artist.redraw()

    def select(self):
        """Select the object representing the item."""
        raise NotImplementedError

    def modify(self):
        """Modify the item represented by the object."""
        raise NotImplementedError

    def move(self):
        """Move the item represented by the object."""
        raise NotImplementedError


# src/compas_rhino/objects/_select.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import ast
import rhinoscriptsyntax as rs


__all__ = [
    'mesh_select_vertex',
    'mesh_select_vertices',
    'mesh_select_face',
    'mesh_select_faces',
    'mesh_select_edge',
    'mesh_select_edges',
    'network_select_node',
    'network_select_nodes',
    'network_select_edge',
    'network_select_edges',
]


def mesh_select_vertex(mesh, message="Select a vertex."):
    """Select a single vertex of a mesh.

    Parameters
    ----------
    mesh: :class:`compas.datastructures.Mesh`
    message: str, optional

    Returns
    -------
    int or None
    """
    guid = rs.GetObject(message, preselect=True, filter=rs.filter.point | rs.filter.textdot)
    if guid:
        prefix = mesh.attributes['name']
        name = rs.ObjectName(guid).split('.')
        if 'vertex' in name:
            if not prefix or prefix in name:
                key = name[-1]
                return ast.literal_eval(key)
    return None


def mesh_select_vertices(mesh, message="Select vertices."):
    """Select multiple vertices of a mesh.

    Parameters
    ----------
    mesh: :class:`compas.datastructures.Mesh`
    message: str, optional

    Returns
    -------
    list of int
    """
    keys = []
    guids = rs.GetObjects(message, preselect=True, filter=rs.filter.point | rs.filter.textdot)
    if guids:
        prefix = mesh.attributes['name']
        seen = set()
        for guid in guids:
            name = rs.ObjectName(guid).split('.')
            if 'vertex' in name:
                if not prefix or prefix in name:
                    key = name[-1]
                    if not seen.add(key):
                        key = ast.literal_eval(key)
                        keys.append(key)
    return keys


def mesh_select_face(mesh, message="Select a face."):
    """Select a single face of a mesh.

    Parameters
    ----------
    mesh: :class:`compas.datastructures.Mesh`
    message: str, optional

    Returns
    -------
    int or None
    """
    guid = rs.GetObject(message, preselect=True, filter=rs.filter.mesh | rs.filter.textdot)
    if guid:
        prefix = mesh.attributes['name']
        name = rs.ObjectName(guid).split('.')
        if 'face' in name:
            if not prefix or prefix in name:
                key = name[-1]
                key = ast.literal_eval(key)
                return key
    return None


def mesh_select_faces(mesh, message="Select faces."):
    """Select multiple faces of a mesh.

    Parameters
    ----------
    mesh: :class:`compas.datastructures.Mesh`
    message: str, optional

    Returns
    -------
    list of int
    """
    keys = []
    guids = rs.GetObjects(message, preselect=True, filter=rs.filter.mesh | rs.filter.textdot)
    if guids:
        prefix = mesh.attributes['name']
        seen = set()
        for guid in guids:
            name = rs.ObjectName(guid).split('.')
            if 'face' in name:
                if not prefix or prefix in name:
                    key = name[-1]
                    if not seen.add(key):
                        key = ast.literal_eval(key)
                        keys.append(key)
    return keys


def mesh_select_edge(mesh, message="Select an edge."):
    """Select a single edge of a mesh.

    Parameters
    ----------
    mesh: :class:`compas.datastructures.Mesh`
    message: str, optional

    Returns
    -------
    tuple of int, or None
    """
    guid = rs.GetObject(message, preselect=True, filter=rs.filter.curve | rs.filter.textdot)
    if guid:
        prefix = mesh.attributes['name']
        name = rs.ObjectName(guid).split('.')
        if 'edge' in name:
            if not prefix or prefix in name:
                key = name[-1]
                u, v = key.split('-')
                u = ast.literal_eval(u)
                v = ast.literal_eval(v)
                return u, v
    return None


def mesh_select_edges(mesh, message="Select edges."):
    """Select multiple edges of a mesh.

    Parameters
    ----------
    mesh: :class:`compas.datastructures.Mesh`
    message: str, optional

    Returns
    -------
    list of tuple of int
    """
    keys = []
    guids = rs.GetObjects(message, preselect=True, filter=rs.filter.curve | rs.filter.textdot)
    if guids:
        prefix = mesh.attributes['name']
        seen = set()
        for guid in guids:
            name = rs.ObjectName(guid).split('.')
            if 'edge' in name:
                if not prefix or prefix in name:
                    key = name[-1]
                    if not seen.add(key):
                        u, v = key.split('-')
                        u = ast.literal_eval(u)
                        v = ast.literal_eval(v)
                        keys.append((u, v))
    return keys


def network_select_node(network, message="Select a node."):
    """Select a single node of a network.

    Parameters
    ----------
    network: :class:`compas.datastructures.Network`
    message: str, optional

    Returns
    -------
    hashable or None
    """
    guid = rs.GetObject(message, preselect=True, filter=rs.filter.point | rs.filter.textdot)
    if guid:
        prefix = network.attributes['name']
        name = rs.ObjectName(guid).split('.')
        if 'node' in name:
            if not prefix or prefix in name:
                key = name[-1]
                return ast.literal_eval(key)
    return None


def network_select_nodes(network, message="Select nodes."):
    """Select multiple nodes of a network.

    Parameters
    ----------
    network: :class:`compas.datastructures.Network`
    message: str, optional

    Returns
    -------
    list of hashable
    """
    keys = []
    guids = rs.GetObjects(message, preselect=True, filter=rs.filter.point | rs.filter.textdot)
    if guids:
        prefix = network.attributes['name']
        seen = set()
        for guid in guids:
            name = rs.ObjectName(guid).split('.')
            if 'node' in name:
                if not prefix or prefix in name:
                    key = name[-1]
                    if not seen.add(key):
                        key = ast.literal_eval(key)
                        keys.append(key)
    return keys


def network_select_edge(network, message="Select an edge."):
    """Select a single edge of a network.

    Parameters
    ----------
    network: :class:`compas.datastructures.Network`
    message: str, optional

    Returns
    -------
    tuple of hashable, or None
    """
    guid = rs.GetObject(message, preselect=True, filter=rs.filter.curve | rs.filter.textdot)
    if guid:
        prefix = network.attributes['name']
        name = rs.ObjectName(guid).split('.')
        if 'edge' in name:
            if not prefix or prefix in name:
                key = name[-1]
                u, v = key.split('-')
                u = ast.literal_eval(u)
                v = ast.literal_eval(v)
                return u, v
    return None


def network_select_edges(network, message="Select edges."):
    """Select multiple edges of a network.

    Parameters
    ----------
    network: :class:`compas.datastructures.Network`
    message: str, optional

    Returns
    -------
    list of tuple of hashable
    """
    keys = []
    guids = rs.GetObjects(message, preselect=True, filter=rs.filter.curve | rs.filter.textdot)
    if guids:
        prefix = network.attributes['name']
        seen = set()
        for guid in guids:
            name = rs.ObjectName(guid).split('.')
            if 'edge' in name:
                if not prefix or prefix in name:
                    key = name[-1]
                    if not seen.add(key):
                        u, v = key.split('-')
                        u = ast.literal_eval(u)
                        v = ast.literal_eval(v)
                        keys.append((u, v))
    return keys


# src/compas_rhino/objects/meshobject.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from functools import reduce
from operator import mul

import Rhino
from Rhino.Geometry import Point3d

import compas_rhino
from compas.geometry import Point
from compas.geometry import Scale
from compas.geometry import Translation
from compas.geometry import Rotation
from compas.geometry import subtract_vectors
from compas.geometry import add_vectors
from compas.geometry import scale_vector

from ._modify import mesh_update_attributes
from ._modify import mesh_update_vertex_attributes
from ._modify import mesh_update_face_attributes
from ._modify import mesh_update_edge_attributes
from ._modify import mesh_move_vertex
from ._modify import mesh_move_vertices
from ._modify import mesh_move_face

from ._object import BaseObject


class MeshObject(BaseObject):
    """Class for representing COMPAS meshes in Rhino.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A mesh data structure.
    scene : :class:`compas.scenes.Scene`, optional
        A scene object.
    name : str, optional
        The name of the object.
    layer : str, optional
        The layer for drawing.
    visible : bool, optional
        Toggle for the visibility of the object.
    settings : dict, optional
        A dictionary of settings.

    """

    SETTINGS = {
        'color.vertices': (255, 255, 255),
        'color.edges': (0, 0, 0),
        'color.faces': (0, 0, 0),
        'color.mesh': (0, 0, 0),
        'show.mesh': True,
        'show.vertices': False,
        'show.edges': False,
        'show.faces': False,
        'show.vertexlabels': False,
        'show.facelabels': False,
        'show.edgelabels': False,
        'show.vertexnormals': False,
        'show.facenormals': False,
    }

    def __init__(self, mesh, scene=None, name=None, layer=None, visible=True, settings=None):
        super(MeshObject, self).__init__(mesh, scene, name, layer, visible)
        self._guids = []
        self._guid_vertex = {}
        self._guid_edge = {}
        self._guid_face = {}
        self._guid_vertexnormal = {}
        self._guid_facenormal = {}
        self._guid_vertexlabel = {}
        self._guid_edgelabel = {}
        self._guid_facelabel = {}
        self._anchor = None
        self._location = None
        self._scale = None
        self._rotation = None
        self.settings.update(type(self).SETTINGS)
        if settings:
            self.settings.update(settings)

    @property
    def mesh(self):
        return self.item

    @mesh.setter
    def mesh(self, mesh):
        self.item = mesh
        self._guids = []
        self._guid_vertex = {}
        self._guid_edge = {}
        self._guid_face = {}
        self._guid_vertexnormal = {}
        self._guid_facenormal = {}
        self._guid_vertexlabel = {}
        self._guid_edgelabel = {}
        self._guid_facelabel = {}

    @property
    def anchor(self):
        """The vertex of the mesh that is anchored to the location of the object."""
        return self._anchor

    @anchor.setter
    def anchor(self, vertex):
        if self.mesh.has_vertex(vertex):
            self._anchor = vertex

    @property
    def location(self):
        """:class:`compas.geometry.Point`:
        The location of the object.
        Default is the origin of the world coordinate system.
        The object transformation is applied relative to this location.

        Setting this location will make a copy of the provided point object.
        Moving the original point will thus not affect the object's location.
        """
        if not self._location:
            self._location = Point(0, 0, 0)
        return self._location

    @location.setter
    def location(self, location):
        self._location = Point(*location)

    @property
    def scale(self):
        """float:
        A uniform scaling factor for the object in the scene.
        The scale is applied relative to the location of the object in the scene.
        """
        if not self._scale:
            self._scale = 1
        return self._scale

    @scale.setter
    def scale(self, scale):
        self._scale = scale

    @property
    def rotation(self):
        """list of float:
        The rotation angles around the 3 axis of the coordinate system
        with the origin placed at the location of the object in the scene.
        """
        if not self._rotation:
            self._rotation = [0, 0, 0]
        return self._rotation

    @rotation.setter
    def rotation(self, rotation):
        self._rotation = rotation

    @property
    def vertex_xyz(self):
        """dict: The view coordinates of the mesh object."""
        origin = Point(0, 0, 0)
        stack = []
        if self.scale != 1:
            S = Scale.from_factors([self.scale] * 3)
            stack.append(S)
        if self.rotation != [0, 0, 0]:
            R = Rotation.from_euler_angles(self.rotation)
            stack.append(R)
        if self.location != origin:
            if self.anchor is not None:
                xyz = self.mesh.vertex_attributes(self.anchor, 'xyz')
                point = Point(* xyz)
                T1 = Translation.from_vector(origin - point)
                stack.insert(0, T1)
            T2 = Translation.from_vector(self.location)
            stack.append(T2)
        if stack:
            X = reduce(mul, stack[::-1])
            mesh = self.mesh.transformed(X)
        else:
            mesh = self.mesh
        vertex_xyz = {vertex: mesh.vertex_attributes(vertex, 'xyz') for vertex in mesh.vertices()}
        return vertex_xyz

    @property
    def guid_vertex(self):
        """dict: Map between Rhino object GUIDs and mesh vertex identifiers."""
        return self._guid_vertex

    @guid_vertex.setter
    def guid_vertex(self, values):
        self._guid_vertex = dict(values)

    @property
    def guid_edge(self):
        """dict: Map between Rhino object GUIDs and mesh edge identifiers."""
        return self._guid_edge

    @guid_edge.setter
    def guid_edge(self, values):
        self._guid_edge = dict(values)

    @property
    def guid_face(self):
        """dict: Map between Rhino object GUIDs and mesh face identifiers."""
        return self._guid_face

    @guid_face.setter
    def guid_face(self, values):
        self._guid_face = dict(values)

    @property
    def guid_vertexnormal(self):
        """dict: Map between Rhino object GUIDs and mesh vertexnormal identifiers."""
        return self._guid_vertexnormal

    @guid_vertexnormal.setter
    def guid_vertexnormal(self, values):
        self._guid_vertexnormal = dict(values)

    @property
    def guid_facenormal(self):
        """dict: Map between Rhino object GUIDs and mesh facenormal identifiers."""
        return self._guid_facenormal

    @guid_facenormal.setter
    def guid_facenormal(self, values):
        self._guid_facenormal = dict(values)

    @property
    def guid_vertexlabel(self):
        """dict: Map between Rhino object GUIDs and mesh vertexlabel identifiers."""
        return self._guid_vertexlabel

    @guid_vertexlabel.setter
    def guid_vertexlabel(self, values):
        self._guid_vertexlabel = dict(values)

    @property
    def guid_facelabel(self):
        """dict: Map between Rhino object GUIDs and mesh facelabel identifiers."""
        return self._guid_facelabel

    @guid_facelabel.setter
    def guid_facelabel(self, values):
        self._guid_facelabel = dict(values)

    @property
    def guid_edgelabel(self):
        """dict: Map between Rhino object GUIDs and mesh edgelabel identifiers."""
        return self._guid_edgelabel

    @guid_edgelabel.setter
    def guid_edgelabel(self, values):
        self._guid_edgelabel = dict(values)

    @property
    def guids(self):
        """list: The GUIDs of all Rhino objects created by this artist."""
        guids = self._guids
        guids += list(self.guid_vertex.keys())
        guids += list(self.guid_edge.keys())
        guids += list(self.guid_face.keys())
        guids += list(self.guid_vertexnormal.keys())
        guids += list(self.guid_facenormal.keys())
        guids += list(self.guid_vertexlabel.keys())
        guids += list(self.guid_edgelabel.keys())
        guids += list(self.guid_facelabel.keys())
        return guids

    def clear(self):
        """Clear all Rhino objects associated with this object.
        """
        compas_rhino.delete_objects(self.guids, purge=True)
        self._guids = []
        self._guid_vertex = {}
        self._guid_edge = {}
        self._guid_face = {}
        self._guid_vertexnormal = {}
        self._guid_facenormal = {}
        self._guid_vertexlabel = {}
        self._guid_edgelabel = {}
        self._guid_facelabel = {}

    def draw(self):
        """Draw the object representing the mesh.
        """
        self.clear()
        if not self.visible:
            return
        self.artist.vertex_xyz = self.vertex_xyz

        if self.settings['show.vertices']:
            vertices = list(self.mesh.vertices())

            guids = self.artist.draw_vertices(vertices=vertices, color=self.settings['color.vertices'])
            self.guid_vertex = zip(guids, vertices)

            if self.settings['show.vertexlabels']:
                text = {vertex: str(vertex) for vertex in vertices}
                guids = self.artist.draw_vertexlabels(text=text, color=self.settings['color.vertices'])
                self.guid_vertexlabel = zip(guids, vertices)

            if self.settings['show.vertexnormals']:
                guids = self.artist.draw_vertexnormals(vertices=vertices, color=self.settings['color.vertices'])
                self.guid_vertexnormal = zip(guids, vertices)

        if self.settings['show.mesh']:
            guids = self.artist.draw_mesh(color=self.settings['color.mesh'], disjoint=True)
            self._guids = guids

        else:
            if self.settings['show.faces']:
                faces = list(self.mesh.faces())

                guids = self.artist.draw_faces(faces=faces, color=self.settings['color.faces'])
                self.guid_face = zip(guids, faces)

                if self.settings['show.facelabels']:
                    text = {face: str(face) for face in faces}
                    guids = self.artist.draw_facelabels(text=text, color=self.settings['color.faces'])
                    self.guid_facelabel = zip(guids, faces)

                if self.settings['show.facenormals']:
                    guids = self.artist.draw_facenormals(faces=faces, color=self.settings['color.faces'])
                    self.guid_face = zip(guids, faces)

        if self.settings['show.edges']:
            edges = list(self.mesh.edges())

            guids = self.artist.draw_edges(edges=edges, color=self.settings['color.edges'])
            self.guid_edge = zip(guids, edges)

            if self.settings['show.edgelabels']:
                text = {edge: "{}-{}".format(*edge) for edge in edges}
                guids = self.artist.draw_edgelabels(text=text, color=self.settings['color.edges'])
                self.guid_edgelabel = zip(guids, edges)

        self.redraw()

    def select(self):
        # there is currently no "general" selection method
        # for the entire mesh object
        raise NotImplementedError

    def select_vertex(self, message="Select one vertex."):
        """Select one vertex of the mesh.

        Returns
        -------
        int
            A vertex identifier.
        """
        guid = compas_rhino.select_point(message=message)
        if guid and guid in self.guid_vertex:
            return self.guid_vertex[guid]

    def select_vertices(self, message="Select vertices."):
        """Select vertices of the mesh.

        Returns
        -------
        list
            A list of vertex identifiers.
        """
        guids = compas_rhino.select_points(message=message)
        vertices = [self.guid_vertex[guid] for guid in guids if guid in self.guid_vertex]
        return vertices

    def select_faces(self, message="Select faces."):
        """Select faces of the mesh.

        Returns
        -------
        list
            A list of face identifiers.
        """
        guids = compas_rhino.select_meshes(message=message)
        faces = [self.guid_face[guid] for guid in guids if guid in self.guid_face]
        return faces

    def select_edges(self, message="Select edges."):
        """Select edges of the mesh.

        Returns
        -------
        list
            A list of edge identifiers.
        """
        guids = compas_rhino.select_lines(message=message)
        edges = [self.guid_edge[guid] for guid in guids if guid in self.guid_edge]
        return edges

    def modify(self):
        """Update the attributes of the mesh.

        Returns
        -------
        bool
            True if the update was successful.
            False otherwise.
        """
        return mesh_update_attributes(self.mesh)

    def modify_vertices(self, vertices, names=None):
        """Update the attributes of selected vertices.

        Parameters
        ----------
        vertices : list
            The vertices of the vertices of which the attributes should be updated.
        names : list, optional
            The names of the attributes that should be updated.
            Default is to update all available attributes.

        Returns
        -------
        bool
            True if the attributes were successfully updated.
            False otherwise.

        """
        return mesh_update_vertex_attributes(self.mesh, vertices, names=names)

    def modify_edges(self, edges, names=None):
        """Update the attributes of the edges.

        Parameters
        ----------
        edges : list
            The edges to update.
        names : list, optional
            The names of the atrtibutes to update.
            Default is to update all attributes.

        Returns
        -------
        bool
            True if the update was successful.
            False otherwise.

        """
        return mesh_update_edge_attributes(self.mesh, edges, names=names)

    def modify_faces(self, faces, names=None):
        """Update the attributes of selected faces.

        Parameters
        ----------
        vertices : list
            The vertices of the vertices of which the attributes should be updated.
        names : list, optional
            The names of the attributes that should be updated.
            Default is to update all available attributes.

        Returns
        -------
        bool
            True if the attributes were successfully updated.
            False otherwise.

        """
        return mesh_update_face_attributes(self.mesh, faces, names=names)

    # not clear if this is now about the location or the data
    def move(self):
        """Move the entire mesh object to a different location."""
        raise NotImplementedError

    def move_vertex(self, vertex):
        """Move a single vertex of the mesh object and update the data structure accordingly.

        Parameters
        ----------
        vertex : int
            The identifier of the vertex.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        return mesh_move_vertex(self.mesh, vertex)

    def move_vertices(self, vertices):
        """Move a multiple vertices of the mesh object and update the data structure accordingly.

        Parameters
        ----------
        vertices : list of int
            The identifiers of the vertices.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        return mesh_move_vertices(self.mesh, vertices)

    def move_face(self, face):
        """Move a single face of the mesh object and update the data structure accordingly.

        Parameters
        ----------
        face : int
            The identifier of the face.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        return mesh_move_face(self.mesh, face)

    def scale_from_3_points(self, message="Select the base vertex for the scaling operation."):
        """Scale the mesh object from 3 reference points.

        Note that this does not scale the underlying data structure,
        but only the scale of the representation in Rhino.

        The first reference point of the scaling operation must be a vertex of the mesh.
        This vertex will become the new anchor of the object.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        def OnDynamicDraw(sender, e):
            d1 = p0.DistanceTo(p1)
            d2 = p0.DistanceTo(e.CurrentPoint)
            ratio = d2 / d1
            DrawLine = e.Display.DrawDottedLine
            for vertex in self.diagram.vertices():
                if vertex == anchor:
                    continue
                vector = vertex_vector[vertex]
                vertex_xyz[vertex] = add_vectors(origin, scale_vector(vector, ratio))
            for u, v in iter(edges):
                DrawLine(Point3d(* vertex_xyz[u]), Point3d(* vertex_xyz[v]), color)

        anchor = self.select_vertex(message=message)
        if anchor is None:
            return

        color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
        edges = list(self.diagram.edges())
        vertex_xyz = self.artist.vertex_xyz
        vertex_xyz0 = {vertex: vertex_xyz[vertex][:] for vertex in vertex_xyz}

        origin = vertex_xyz0[anchor]
        vertex_vector = {vertex: subtract_vectors(vertex_xyz0[vertex], origin) for vertex in vertex_xyz}
        p0 = Point3d(* origin)

        gp = Rhino.Input.Custom.GetPoint()
        gp.SetCommandPrompt('Select the 1st reference point.')
        gp.Get()
        if gp.CommandResult() != Rhino.Commands.Result.Success:
            return False
        p1 = gp.Point()

        gp.SetCommandPrompt('Select the 2nd reference point.')
        gp.DynamicDraw += OnDynamicDraw
        gp.Get()
        if gp.CommandResult() != Rhino.Commands.Result.Success:
            return False
        p2 = gp.Point()

        d1 = p0.DistanceTo(p1)
        d2 = p0.DistanceTo(p2)
        ratio = d2 / d1
        self.scale *= ratio
        self.anchor = anchor
        self.location = vertex_xyz[anchor]
        return True


# src/compas_rhino/objects/networkobject.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from functools import reduce
from operator import mul

import compas_rhino

from compas.geometry import Point
from compas.geometry import Scale
from compas.geometry import Translation
from compas.geometry import Rotation

from ._modify import network_update_attributes
from ._modify import network_update_node_attributes
from ._modify import network_update_edge_attributes
from ._modify import network_move_node

from ._object import BaseObject


class NetworkObject(BaseObject):
    """Class for representing COMPAS networkes in Rhino.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network data structure.
    scene : :class:`compas.scenes.Scene`, optional
        A scene object.
    name : str, optional
        The name of the object.
    layer : str, optional
        The layer for drawing.
    visible : bool, optional
        Toggle for the visibility of the object.
    settings : dict, optional
        A dictionary of settings.

    """

    SETTINGS = {
        'color.nodes': (255, 255, 255),
        'color.edges': (0, 0, 0),
        'show.nodes': True,
        'show.edges': True,
        'show.nodelabels': False,
        'show.edgelabels': False,
    }

    def __init__(self, network, scene=None, name=None, layer=None, visible=True, settings=None):
        super(NetworkObject, self).__init__(network, scene, name, layer, visible)
        self._guid_node = {}
        self._guid_edge = {}
        self._guid_nodelabel = {}
        self._guid_edgelabel = {}
        self._anchor = None
        self._location = None
        self._scale = None
        self._rotation = None
        self.settings.update(NetworkObject.SETTINGS)
        if settings:
            self.settings.update(settings)

    @property
    def network(self):
        return self.item

    @network.setter
    def network(self, network):
        self.item = network
        self._guids = []
        self._guid_node = {}
        self._guid_edge = {}
        self._guid_nodelabel = {}
        self._guid_edgelabel = {}

    @property
    def anchor(self):
        """The node of the network that is anchored to the location of the object."""
        return self._anchor

    @anchor.setter
    def anchor(self, node):
        if self.network.has_node(node):
            self._anchor = node

    @property
    def location(self):
        """:class:`compas.geometry.Point`:
        The location of the object.
        Default is the origin of the world coordinate system.
        The object transformation is applied relative to this location.

        Setting this location will make a copy of the provided point object.
        Moving the original point will thus not affect the object's location.
        """
        if not self._location:
            self._location = Point(0, 0, 0)
        return self._location

    @location.setter
    def location(self, location):
        self._location = Point(*location)

    @property
    def scale(self):
        """float:
        A uniform scaling factor for the object in the scene.
        The scale is applied relative to the location of the object in the scene.
        """
        if not self._scale:
            self._scale = 1.0
        return self._scale

    @scale.setter
    def scale(self, scale):
        self._scale = scale

    @property
    def rotation(self):
        """list of float:
        The rotation angles around the 3 axis of the coordinate system
        with the origin placed at the location of the object in the scene.
        """
        if not self._rotation:
            self._rotation = [0, 0, 0]
        return self._rotation

    @rotation.setter
    def rotation(self, rotation):
        self._rotation = rotation

    @property
    def node_xyz(self):
        """dict : The view coordinates of the mesh object."""
        origin = Point(0, 0, 0)
        stack = []
        if self.scale != 1.0:
            S = Scale.from_factors([self.scale] * 3)
            stack.append(S)
        if self.rotation != [0, 0, 0]:
            R = Rotation.from_euler_angles(self.rotation)
            stack.append(R)
        if self.location != origin:
            if self.anchor is not None:
                xyz = self.network.vertex_attributes(self.anchor, 'xyz')
                point = Point(* xyz)
                T1 = Translation.from_vector(origin - point)
                stack.insert(0, T1)
            T2 = Translation.from_vector(self.location)
            stack.append(T2)
        if stack:
            X = reduce(mul, stack[::-1])
            network = self.network.transformed(X)
        else:
            network = self.network
        node_xyz = {network: network.node_attributes(node, 'xyz') for node in network.nodes()}
        return node_xyz

    @property
    def guid_node(self):
        """dict: Map between Rhino object GUIDs and network node identifiers."""
        if not self._guid_node:
            self._guid_node = {}
        return self._guid_node

    @guid_node.setter
    def guid_node(self, values):
        self._guid_node = dict(values)

    @property
    def guid_edge(self):
        """dict: Map between Rhino object GUIDs and network edge identifiers."""
        if not self._guid_edge:
            self._guid_edge = {}
        return self._guid_edge

    @guid_edge.setter
    def guid_edge(self, values):
        self._guid_edge = dict(values)

    @property
    def guid_nodelabel(self):
        """dict: Map between Rhino object GUIDs and network nodelabel identifiers."""
        return self._guid_vertexlabel

    @guid_nodelabel.setter
    def guid_nodelabel(self, values):
        self._guid_vertexlabel = dict(values)

    @property
    def guid_edgelabel(self):
        """dict: Map between Rhino object GUIDs and network edgelabel identifiers."""
        return self._guid_edgelabel

    @guid_edgelabel.setter
    def guid_edgelabel(self, values):
        self._guid_edgelabel = dict(values)

    @property
    def guids(self):
        guids = []
        guids += list(self.guid_node)
        guids += list(self.guid_edge)
        return guids

    def clear(self):
        compas_rhino.delete_objects(self.guids, purge=True)
        self._guid_node = {}
        self._guid_edge = {}

    def draw(self):
        """Draw the object representing the network.
        """
        self.clear()
        if not self.visible:
            return

        self.artist.node_xyz = self.node_xyz

        if self.settings['show.nodes']:
            nodes = list(self.network.nodes())

            guids = self.artist.draw_nodes(nodes=nodes, color=self.settings['color.nodes'])
            self.guid_node = zip(guids, nodes)

            if self.settings['show.nodelabels']:
                text = {node: str(node) for node in nodes}
                guids = self.artist.draw_nodelabels(text=text, color=self.settings['color.nodes'])
                self.guid_nodelabel = zip(guids, nodes)

        if self.settings['show.edges']:
            edges = list(self.network.edges())

            guids = self.artist.draw_edges(edges=edges, color=self.settings['color.edges'])
            self.guid_edge = zip(guids, edges)

            if self.settings['show.edgelabels']:
                text = {edge: "{}-{}".format(*edge) for edge in edges}
                guids = self.artist.draw_edgelabels(text=text, color=self.settings['color.edges'])
                self.guid_edgelabel = zip(guids, edges)

        self.redraw()

    def select(self):
        # there is currently no "general" selection method
        # for the entire mesh object
        raise NotImplementedError

    def select_nodes(self):
        """Select nodes of the network.

        Returns
        -------
        list
            A list of node identifiers.
        """
        guids = compas_rhino.select_points()
        nodes = [self.guid_node[guid] for guid in guids if guid in self.guid_node]
        return nodes

    def select_edges(self):
        """Select edges of the network.

        Returns
        -------
        list
            A list of edge identifiers.
        """
        guids = compas_rhino.select_lines()
        edges = [self.guid_edge[guid] for guid in guids if guid in self.guid_edge]
        return edges

    def modify(self):
        """Update the attributes of the network.

        Returns
        -------
        bool
            True if the update was successful.
            False otherwise.
        """
        return network_update_attributes(self.network)

    def modify_nodes(self, nodes, names=None):
        """Update the attributes of the nodes.

        Parameters
        ----------
        nodes : list
            The identifiers of the nodes to update.
        names : list, optional
            The names of the atrtibutes to update.
            Default is to update all attributes.

        Returns
        -------
        bool
            True if the update was successful.
            False otherwise.

        """
        return network_update_node_attributes(self.network, nodes, names=names)

    def modify_edges(self, edges, names=None):
        """Update the attributes of the edges.

        Parameters
        ----------
        edges : list
            The identifiers of the edges to update.
        names : list, optional
            The names of the atrtibutes to update.
            Default is to update all attributes.

        Returns
        -------
        bool
            True if the update was successful.
            False otherwise.
        """
        return network_update_edge_attributes(self.network, edges, names=names)

    def move(self):
        """Move the entire mesh object to a different location."""
        raise NotImplementedError

    def move_node(self, node):
        """Move a single node of the network object and update the data structure accordingly.

        Parameters
        ----------
        node : int
            The identifier of the node.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        return network_move_node(self.network, node)


# src/compas_rhino/objects/volmeshobject.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from functools import reduce
from operator import mul

import compas_rhino

from compas.geometry import Point
from compas.geometry import Scale
from compas.geometry import Translation
from compas.geometry import Rotation

from ._modify import mesh_update_attributes
from ._modify import mesh_update_vertex_attributes
from ._modify import mesh_update_face_attributes
from ._modify import mesh_update_edge_attributes
from ._modify import mesh_move_vertex
from ._modify import mesh_move_vertices
from ._modify import mesh_move_face

from ._object import BaseObject


class VolMeshObject(BaseObject):
    """Class for representing COMPAS volmeshes in Rhino.

    Parameters
    ----------
    volmesh : :class:`compas.datastructures.VolMesh`
        A volmesh data structure.
    scene : :class:`compas.scenes.Scene`, optional
        A scene object.
    name : str, optional
        The name of the object.
    layer : str, optional
        The layer for drawing.
    visible : bool, optional
        Toggle for the visibility of the object.
    settings : dict, optional
        A dictionary of settings.

    """

    SETTINGS = {
        'color.vertices': (255, 255, 255),
        'color.edges': (0, 0, 0),
        'color.faces': (210, 210, 210),
        'color.cells': (255, 0, 0),
        'show.vertices': True,
        'show.edges': True,
        'show.faces': True,
        'show.cells': False,
        'show.vertexlabels': False,
        'show.facelabels': False,
        'show.edgelabels': False,
        'show.celllabels': False,
    }

    # modify = mesh_update_attributes
    # modify_vertices = mesh_update_vertex_attributes
    # modify_faces = mesh_update_face_attributes
    # modify_edges = mesh_update_edge_attributes

    def __init__(self, volmesh, scene=None, name=None, layer=None, visible=True, settings=None):
        super(VolMeshObject, self).__init__(volmesh, scene, name, layer, visible)
        self._guids = []
        self._guid_vertex = {}
        self._guid_edge = {}
        self._guid_face = {}
        self._guid_cell = {}
        self._guid_vertexlabel = {}
        self._guid_edgelabel = {}
        self._guid_facelabel = {}
        self._guid_celllabel = {}
        self._anchor = None
        self._location = None
        self._scale = None
        self._rotation = None
        self.settings.update(VolMeshObject.SETTINGS)
        if settings:
            self.settings.update(settings)

    @property
    def volmesh(self):
        return self.item

    @volmesh.setter
    def volmesh(self, volmesh):
        self.item = volmesh

    @property
    def anchor(self):
        """The vertex of the volmesh that is anchored to the location of the object."""
        return self._anchor

    @anchor.setter
    def anchor(self, vertex):
        if self.volmesh.has_vertex(vertex):
            self._anchor = vertex

    @property
    def location(self):
        """:class:`compas.geometry.Point`:
        The location of the object.
        Default is the origin of the world coordinate system.
        The object transformation is applied relative to this location.

        Setting this location will make a copy of the provided point object.
        Moving the original point will thus not affect the object's location.
        """
        if not self._location:
            self._location = Point(0, 0, 0)
        return self._location

    @location.setter
    def location(self, location):
        self._location = Point(*location)

    @property
    def scale(self):
        """float:
        A uniform scaling factor for the object in the scene.
        The scale is applied relative to the location of the object in the scene.
        """
        if not self._scale:
            self._scale = 1.0
        return self._scale

    @scale.setter
    def scale(self, scale):
        self._scale = scale

    @property
    def rotation(self):
        """list of float:
        The rotation angles around the 3 axis of the coordinate system
        with the origin placed at the location of the object in the scene.
        """
        if not self._rotation:
            self._rotation = [0, 0, 0]
        return self._rotation

    @rotation.setter
    def rotation(self, rotation):
        self._rotation = rotation

    @property
    def vertex_xyz(self):
        """dict : The view coordinates of the volmesh object."""
        origin = Point(0, 0, 0)
        stack = []
        if self.scale != 1.0:
            S = Scale.from_factors([self.scale] * 3)
            stack.append(S)
        if self.rotation != [0, 0, 0]:
            R = Rotation.from_euler_angles(self.rotation)
            stack.append(R)
        if self.location != origin:
            if self.anchor is not None:
                xyz = self.volmesh.vertex_attributes(self.anchor, 'xyz')
                point = Point(* xyz)
                T1 = Translation.from_vector(origin - point)
                stack.insert(0, T1)
            T2 = Translation.from_vector(self.location)
            stack.append(T2)
        if stack:
            X = reduce(mul, stack[::-1])
            volmesh = self.volmesh.transformed(X)
        else:
            volmesh = self.volmesh
        vertex_xyz = {vertex: volmesh.vertex_attributes(vertex, 'xyz') for vertex in volmesh.vertices()}
        return vertex_xyz

    @property
    def guid_vertex(self):
        """Map between Rhino object GUIDs and volmesh vertex identifiers."""
        return self._guid_vertex

    @guid_vertex.setter
    def guid_vertex(self, values):
        self._guid_vertex = dict(values)

    @property
    def guid_edge(self):
        """Map between Rhino object GUIDs and volmsh edge identifiers."""
        return self._guid_edge

    @guid_edge.setter
    def guid_edge(self, values):
        self._guid_edge = dict(values)

    @property
    def guid_face(self):
        """Map between Rhino object GUIDs and volmesh face identifiers."""
        return self._guid_face

    @guid_face.setter
    def guid_face(self, values):
        self._guid_face = dict(values)

    @property
    def guid_cell(self):
        """Map between Rhino object GUIDs and volmesh face identifiers."""
        return self._guid_cell

    @guid_cell.setter
    def guid_cell(self, values):
        self._guid_cell = dict(values)

    @property
    def guid_vertexlabel(self):
        """Map between Rhino object GUIDs and volmesh vertexlabel identifiers."""
        return self._guid_vertexlabel

    @guid_vertexlabel.setter
    def guid_vertexlabel(self, values):
        self._guid_vertexlabel = dict(values)

    @property
    def guid_edgelabel(self):
        """Map between Rhino object GUIDs and volmesh edgelabel identifiers."""
        return self._guid_edgelabel

    @guid_edgelabel.setter
    def guid_edgelabel(self, values):
        self._guid_edgelabel = dict(values)

    @property
    def guid_facelabel(self):
        """Map between Rhino object GUIDs and volmesh facelabel identifiers."""
        return self._guid_facelabel

    @guid_facelabel.setter
    def guid_facelabel(self, values):
        self._guid_facelabel = dict(values)

    @property
    def guid_celllabel(self):
        """Map between Rhino object GUIDs and volmesh celllabel identifiers."""
        return self._guid_celllabel

    @guid_celllabel.setter
    def guid_celllabel(self, values):
        self._guid_celllabel = dict(values)

    @property
    def guids(self):
        """list: The GUIDs of all Rhino objects created by this artist."""
        guids = self._guids
        guids += list(self.guid_vertex)
        guids += list(self.guid_edge)
        guids += list(self.guid_face)
        guids += list(self.guid_cell)
        guids += list(self.guid_vertexlabel)
        guids += list(self.guid_edgelabel)
        guids += list(self.guid_facelabel)
        guids += list(self.guid_celllabel)
        return guids

    def clear(self):
        """Clear all objects previously drawn by this artist.
        """
        compas_rhino.delete_objects(self.guids, purge=True)
        self._guid_vertex = {}
        self._guid_edge = {}
        self._guid_face = {}
        self._guid_cell = {}
        self._guid_vertexlabel = {}
        self._guid_edgelabel = {}
        self._guid_facelabel = {}
        self._guid_celllabel = {}

    def draw(self):
        """Draw the volmesh using the visualisation settings.
        """
        self.clear()
        if not self.visible:
            return

        self.artist.vertex_xyz = self.vertex_xyz

        if self.settings['show.vertices']:
            vertices = list(self.volmesh.vertices())
            guids = self.artist.draw_vertices(vertices=vertices, color=self.settings['color.vertices'])
            self.guid_vertex = zip(guids, vertices)

            if self.settings['show.vertexlabels']:
                text = {vertex: str(vertex) for vertex in vertices}
                guids = self.artist.draw_vertexlabels(text=text, color=self.settings['color.vertices'])
                self.guid_vertexlabel = zip(guids, vertices)

        if self.settings['show.faces']:
            faces = list(self.volmesh.faces())
            guids = self.artist.draw_faces(faces=faces, color=self.settings['color.faces'])
            self.guid_face = zip(guids, faces)

            if self.settings['show.facelabels']:
                text = {face: str(face) for face in faces}
                guids = self.artist.draw_facelabels(text=text, color=self.settings['color.faces'])
                self.guid_facelabel = zip(guids, faces)

        if self.settings['show.edges']:
            edges = list(self.volmesh.edges())
            guids = self.artist.draw_edges(edges=edges, color=self.settings['color.edges'])
            self.guid_edge = zip(guids, edges)

            if self.settings['show.edgelabels']:
                text = {edge: "{}-{}".format(*edge) for edge in edges}
                guids = self.artist.draw_edgelabels(text=text, color=self.settings['color.edges'])
                self.guid_edgelabel = zip(guids, edges)

        if self.settings['show.cells']:
            cells = list(self.volmesh.cells())
            guids = self.artist.draw_cells(cells=cells, color=self.settings['color.cells'])
            self.guid_cell = zip(guids, cells)

            if self.settings['show.celllabels']:
                text = {cell: str(cell) for cell in cells}
                guids = self.artist.draw_celllabels(text=text, color=self.settings['color.cells'])
                self.guid_edgelabel = zip(guids, edges)

        self.redraw()

    def select(self):
        # there is currently no "general" selection method
        # for the entire mesh object
        raise NotImplementedError

    def select_vertex(self, message="Select one vertex."):
        """Select one vertex of the mesh.

        Returns
        -------
        int
            A vertex identifier.
        """
        guid = compas_rhino.select_point(message=message)
        if guid and guid in self.guid_vertex:
            return self.guid_vertex[guid]

    def select_vertices(self, message="Select vertices."):
        """Select vertices of the volmesh.

        Returns
        -------
        list
            A list of vertex identifiers.
        """
        guids = compas_rhino.select_points(message=message)
        vertices = [self.guid_vertex[guid] for guid in guids if guid in self.guid_vertex]
        return vertices

    def select_faces(self, message="Select faces."):
        """Select faces of the volmesh.

        Returns
        -------
        list
            A list of face identifiers.
        """
        guids = compas_rhino.select_meshes(message=message)
        faces = [self.guid_face[guid] for guid in guids if guid in self.guid_face]
        return faces

    def select_edges(self, message="Select edges."):
        """Select edges of the volmesh.

        Returns
        -------
        list
            A list of edge identifiers.
        """
        guids = compas_rhino.select_lines(message=message)
        edges = [self.guid_edge[guid] for guid in guids if guid in self.guid_edge]
        return edges

    def modify(self):
        """Update the attributes of the volmesh.

        Returns
        -------
        bool
            True if the update was successful.
            False otherwise.
        """
        return mesh_update_attributes(self.volmesh)

    def modify_vertices(self, vertices, names=None):
        """Update the attributes of selected vertices.

        Parameters
        ----------
        vertices : list
            The vertices of the vertices of which the attributes should be updated.
        names : list, optional
            The names of the attributes that should be updated.
            Default is to update all available attributes.

        Returns
        -------
        bool
            True if the attributes were successfully updated.
            False otherwise.

        """
        return mesh_update_vertex_attributes(self.volmesh, vertices, names=names)

    def modify_edges(self, edges, names=None):
        """Update the attributes of the edges.

        Parameters
        ----------
        edges : list
            The edges to update.
        names : list, optional
            The names of the atrtibutes to update.
            Default is to update all attributes.

        Returns
        -------
        bool
            True if the update was successful.
            False otherwise.

        """
        return mesh_update_edge_attributes(self.volmesh, edges, names=names)

    def modify_faces(self, faces, names=None):
        """Update the attributes of selected faces.

        Parameters
        ----------
        vertices : list
            The vertices of the vertices of which the attributes should be updated.
        names : list, optional
            The names of the attributes that should be updated.
            Default is to update all available attributes.

        Returns
        -------
        bool
            True if the attributes were successfully updated.
            False otherwise.

        """
        return mesh_update_face_attributes(self.volmesh, faces, names=names)

    def move(self):
        """Move the entire volmesh object to a different location."""
        raise NotImplementedError

    def move_vertex(self, vertex):
        """Move a single vertex of the volmesh object and update the data structure accordingly.

        Parameters
        ----------
        vertex : int
            The identifier of the vertex.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        return mesh_move_vertex(self.volmesh, vertex)

    def move_vertices(self, vertices):
        """Move a multiple vertices of the volmesh object and update the data structure accordingly.

        Parameters
        ----------
        vertices : list of int
            The identifiers of the vertices.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        return mesh_move_vertices(self.volmesh, vertices)

    # it is not entirely clear what is meant with this in terms of face/halfface
    def move_face(self, face):
        """Move a single face of the volmesh object and update the data structure accordingly.

        Parameters
        ----------
        face : int
            The identifier of the face.

        Returns
        -------
        bool
            True if the operation was successful.
            False otherwise.
        """
        return mesh_move_face(self.volmesh, face)


# src/compas_rhino/ui/__init__.py

"""
********************************************************************************
ui
********************************************************************************

.. currentmodule:: compas_rhino.ui

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    CommandMenu
    Mouse
    Rui

"""

from .command import (
    CommandMenu,
    CommandAction
)
from .mouse import Mouse
from .rui import Rui

import warnings

warnings.warn(
    "The ui module in compas_rhino is deprecated. Use the equivalent functionality from compas_ui instead",
    DeprecationWarning,
    stacklevel=2
)

__all__ = [
    'CommandMenu',
    'CommandAction',
    'Mouse',
    'Rui'
]


# src/compas_rhino/ui/command.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import compas_rhino


__all__ = ['CommandMenu', 'CommandAction']


class CommandMenu(object):
    """"""

    def __init__(self, menu):
        self.menu = menu

    def select_action(self):
        def _select(message, options):
            if not options:
                return
            names = [option["name"] for option in options]
            name = compas_rhino.rs.GetString(message, names[0], names)
            if not name:
                return
            if name not in names:
                raise Exception("This option is not valid: {}".format(name))
            for option in options:
                if option["name"] == name:
                    break
            if "action" in option:
                return option

            message = option["message"]
            options = option.get("options")

            return _select(message, options)

        return _select(self.menu["message"], self.menu["options"])


class CommandAction(object):
    """"""

    def __init__(self, name, action):
        self.name = name
        self.action = action

    def __call__(self, *args, **kwargs):
        return self.action(*args, **kwargs)


# ==============================================================================
# Main
# ==============================================================================

if __name__ == "__main__":

    config = {
        "message": "FormDiagram Select",
        "options": [
            {"name": "Vertices", "message": "Select Vertices", "options": [
                {"name": "Boundary", "action": None},
                {"name": "Continuous", "action": None},
                {"name": "Parallel", "action": None},
            ]},
            {"name": "Edges", "message": "Select Edges", "options": [
                {"name": "Boundary", "action": None},
                {"name": "Continuous", "action": None},
                {"name": "Parallel", "action": None},
            ]},
            {"name": "Faces", "message": "Select Faces", "options": [
                {"name": "Boundary", "action": None},
                {"name": "Continuous", "action": None},
                {"name": "Parallel", "action": None},
            ]}
        ]
    }

    menu = CommandMenu(config)
    action = menu.select_action()
    print(action)


# src/compas_rhino/ui/mouse.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division


import Rhino.UI


__all__ = ['Mouse']


class Mouse(Rhino.UI.MouseCallback):
    """"""

    def __init__(self, parent=None):
        super(Mouse, self).__init__()
        self.parent = parent
        self.x = None  # x-coordinate of 2D point in the viewport
        self.y = None  # y-coordinate of 2D point in the viewport
        self.p1 = None  # start of the frustum line in world coordinates
        self.p2 = None  # end of the frustum line in world coordinates

    def OnMouseMove(self, e):
        line = e.View.ActiveViewport.ClientToWorld(e.ViewportPoint)
        self.x = e.ViewportPoint.X
        self.y = e.ViewportPoint.Y
        self.p1 = line.From
        self.p2 = line.To
        e.View.Redraw()

    def OnMouseDown(self, e):
        pass

    def OnMouseUp(self, e):
        pass


# src/compas_rhino/ui/rui.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import os
import inspect
import uuid

import compas

if not compas.IPY:
    from xml.etree import ElementTree as ET
    from xml.dom import minidom


__all__ = ['Rui']


TPL_RUI = """<?xml version="1.0" encoding="utf-8"?>
<RhinoUI major_ver="2"
         minor_ver="0"
         guid="{0}"
         localize="False"
         default_language_id="1033">
    <extend_rhino_menus>
        <menu guid="{1}">
          <text>
            <locale_1033>Extend Rhino Menus</locale_1033>
          </text>
        </menu>
    </extend_rhino_menus>
    <menus />
    <tool_bar_groups />
    <tool_bars />
    <macros />
    <bitmaps>
        <small_bitmap item_width="16" item_height="16">
          <bitmap>iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6Q
AAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAATSURBVDhPYxgFo2AUjAIwYGAAAA
QQAAGnRHxjAAAAAElFTkSuQmCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</bitmap>
        </small_bitmap>
        <normal_bitmap item_width="24" item_height="24">
          <bitmap>iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6Q
AAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYSURBVEhL7cEBAQAAAIIg/6+uIU
AAAFwNCRgAAdACW14AAAAASUVORK5CYIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</bitmap>
        </normal_bitmap>
        <large_bitmap item_width="32" item_height="32">
          <bitmap>iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6Q
AAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAaSURBVFhH7cEBAQAAAIIg/69uSE
AAAADAuRoQIAABnXhJQwAAAABJRU5ErkJgggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</bitmap>
        </large_bitmap>
    </bitmaps>
    <scripts />
</RhinoUI>
"""

TPL_MACRO = """
<macro_item guid="{0}">
    <text>
        <locale_1033>{1}</locale_1033>
    </text>
    <script>{2}</script>
    <tooltip>
        <locale_1033>{3}</locale_1033>
    </tooltip>
    <help_text>
        <locale_1033>{4}</locale_1033>
    </help_text>
    <button_text>
        <locale_1033>{5}</locale_1033>
    </button_text>
    <menu_text>
        <locale_1033>{6}</locale_1033>
    </menu_text>
</macro_item>
"""

TPL_MENUITEM = """
<menu_item guid="{0}" item_type="normal">
    <macro_id>{1}</macro_id>
</menu_item>
"""

TPL_MENUSEPARATOR = """
<menu_item guid="{0}" item_type="separator"></menu_item>
"""

TPL_TOOLBARITEM = """
<tool_bar_item guid="{0}" button_display_mode="control_only" button_style="normal">
    <left_macro_id>{1}</left_macro_id>
    <right_macro_id>{2}</right_macro_id>
</tool_bar_item>
"""

TPL_TOOLBARSEPARATOR = """
<tool_bar_item guid="{0}" button_display_mode="control_only" button_style="spacer">
</tool_bar_item>
"""

TPL_TOOLBAR = """
<tool_bar guid="{0}" item_display_style="{2[item_display_style]}">
    <text>
        <locale_1033>{1}</locale_1033>
    </text>
</tool_bar>
"""

TPL_TOOLBARGROUP = """
<tool_bar_group guid="{0}"
                dock_bar_guid32=""
                dock_bar_guid64=""
                active_tool_bar_group=""
                single_file="{2[single_file]}"
                hide_single_tab="{2[hide_single_tab]}"
                point_floating="0,0">
    <text>
        <locale_1033>{1}</locale_1033>
    </text>
</tool_bar_group>
"""

TPL_TOOLBARGROUPITEM = """
<tool_bar_group_item guid="{0}" major_version="1" minor_version="1">
    <text>
        <locale_1033>{1}</locale_1033>
    </text>
    <tool_bar_id>{2}</tool_bar_id>
</tool_bar_group_item>
"""


def get_method_comments(obj):
    return get_parsed_object_comments(obj)


def get_object_comments(obj):
    return inspect.getcomments(obj)


def get_parsed_object_comments(obj, separator='=>', defaults=None):
    options = {}
    if defaults:
        options.update(defaults)
    comments = get_object_comments(obj)
    if comments:
        comments = [comment.lstrip('#').strip() for comment in comments.split('\n')]
        comments = [comment.split(separator) for comment in comments]
        comments = [comment for comment in comments if len(comment) == 2]
        comments = dict((key.strip(), value.strip()) for key, value in comments)
        options.update(comments)
    return options


def get_method_lineno(obj):
    return inspect.getsourcelines(obj)[1]


def get_method_docstring(obj):
    return inspect.getdoc(obj)


def get_method_oneliner(obj):
    docstring = get_method_docstring(obj)
    if not docstring:
        return
    parts = docstring.trim().split('\n')
    return parts[0]


def get_methods(obj, bysource=False):
    if compas.PY3:
        methods = inspect.getmembers(obj, inspect.isfunction)
    else:
        methods = inspect.getmembers(obj, inspect.ismethod)
    if not bysource:
        return methods
    linenumbers = dict((method[0], get_method_lineno(method[1])) for method in methods)
    return sorted(methods, key=lambda method: linenumbers[method[0]])


def get_public_methods(obj):
    methods = get_methods(obj)
    return [method for method in methods if not method[0].startswith('_')]


# def get_macros(controller, instance_name):
#     methods = get_public_methods(controller)
#     macros = []
#     for n, o in methods:
#         annotations = get_parsed_object_comments(o, separator='=>')
#         name = annotations.get('name') or n
#         script = annotations.get('script') or '-_RunPythonScript ({0}.{1}())'.format(instance_name, name)
#         tooltip = annotations.get('tooltip')
#         help_text = annotations.get('help_text') or tooltip
#         text = annotations.get('text') or name
#         button_text = annotations.get('button_text') or text
#         menu_text = annotations.get('menu_text') or ' '.join(text.split('_'))
#         macros.append({
#             'name': instance_name + '.' + name,
#             'script': script,
#             'tooltip': tooltip,
#             'help_text': help_text,
#             'button_text': button_text,
#             'menu_text': menu_text,
#         })
#     return macros


# def find_macro(name, macros):
#     for i, macro in enumerate(macros):
#         if macro['name'] == name:
#             return i, macro
#     return None, None


# def update_macro(macros, name, key, value):
#     i, macro = find_macro(name, macros)
#     if macro:
#         macro[key] = value
#     macros[i] = macro


class Rui(object):
    """Class for generating RUI files.

    Parameters
    ----------
    filepath : str
        Path to the RUI file.

    """

    def __init__(self, filepath):
        self.filepath = filepath
        self.macros = {}
        self.toolbars = {}
        self.xml = None
        self.root = None
        self.root_macros = []
        self.root_menus = []
        self.root_toolbargroups = []
        self.root_toolbars = []
        self.check()

    def check(self):
        if not os.path.exists(os.path.dirname(self.filepath)):
            try:
                os.makedirs(os.path.dirname(self.filepath))
            except OSError as e:
                if e.errno != os.errno.EEXIST:
                    raise e
        if not os.path.exists(self.filepath):
            with open(self.filepath, 'w+'):
                pass

    def init(self):
        with open(self.filepath, 'w+') as f:
            f.write(TPL_RUI.format(uuid.uuid4(), uuid.uuid4()))
        self.xml = ET.parse(self.filepath)
        self.root = self.xml.getroot()
        self.root_macros = self.root.find('macros')
        self.root_menus = self.root.find('menus')
        self.root_toolbargroups = self.root.find('tool_bar_groups')
        self.root_toolbars = self.root.find('tool_bars')

    def parse(self):
        raise NotImplementedError

    def write(self):
        root = ET.tostring(self.root)
        xml = minidom.parseString(root).toprettyxml(indent="  ")
        xml = '\n'.join([line for line in xml.split('\n') if line.strip()])
        with open(self.filepath, 'w+') as fh:
            fh.write(xml.encode('utf-8'))

    # --------------------------------------------------------------------------
    # add macros
    # --------------------------------------------------------------------------

    def add_macros(self, macros):
        for macro in macros:
            guid = str(uuid.uuid4())
            name = macro['name']
            script = macro['script']
            tooltip = macro.get('tooltip', '')
            help_text = macro.get('help_text', '')
            button_text = macro.get('button_text', name)
            menu_text = macro.get('menu_text', name.replace('_', ' '))
            self.add_macro(name, guid, script, tooltip, help_text, button_text, menu_text)

    def add_macro(self, name, guid, script, tooltip, help_text, button_text, menu_text):
        s_macro = TPL_MACRO.format(guid, name, script, tooltip, help_text, button_text, menu_text)
        e_macro = ET.fromstring(s_macro)
        self.root_macros.append(e_macro)
        self.macros[name] = e_macro

    # --------------------------------------------------------------------------
    # add menus
    # --------------------------------------------------------------------------

    def add_menus(self, menus):
        for menu in menus:
            self.add_menu(menu)

    def add_menu(self, menu, root=None):
        if root is None:
            root = self.root_menus
        e_menu = ET.SubElement(root, 'menu')
        e_menu.set('guid', str(uuid.uuid4()))
        e_text = ET.SubElement(e_menu, 'text')
        e_locale = ET.SubElement(e_text, 'locale_1033')
        e_locale.text = menu['name']
        for item in menu['items']:
            if item['type'] == 'normal':
                e_macro = self.macros[item['macro']]
                macro_guid = e_macro.attrib['guid']
                self.add_menuitem(e_menu, macro_guid)
                continue
            if item['type'] == 'separator':
                self.add_menuseparator(e_menu)
                continue
            if item['type'] == 'submenu':
                self.add_menu(item, root=e_menu)
                continue

    def add_menuitem(self, root, macro_id):
        guid = uuid.uuid4()
        s_item = TPL_MENUITEM.format(guid, macro_id)
        e_item = ET.fromstring(s_item)
        root.append(e_item)

    def add_menuseparator(self, root):
        guid = uuid.uuid4()
        s_sep = TPL_MENUSEPARATOR.format(guid)
        e_sep = ET.fromstring(s_sep)
        root.append(e_sep)

    # --------------------------------------------------------------------------
    # add toolbars
    # --------------------------------------------------------------------------

    def add_toolbars(self, toolbars):
        for toolbar in toolbars:
            self.add_toolbar(toolbar)

    def add_toolbar(self, toolbar):
        options = {
            'item_display_style': 'text_only'
        }
        guid = uuid.uuid4()
        s_tb = TPL_TOOLBAR.format(guid, toolbar['name'], options)
        e_tb = ET.fromstring(s_tb)
        self.root_toolbars.append(e_tb)
        self.toolbars[toolbar['name']] = e_tb
        for item in toolbar['items']:
            if item.get('type', 'normal') == 'normal':
                left_guid = None
                left_macro = item.get('left_macro', item.get('left'))
                if left_macro:
                    e_left = self.macros[left_macro]
                    left_guid = e_left.attrib['guid']
                right_guid = None
                right_macro = item.get('right_macro', item.get('right'))
                if right_macro:
                    e_right = self.macros[right_macro]
                    right_guid = e_right.attrib['guid']
                self.add_toolbaritem(e_tb, left_guid, right_guid)
                continue
            if item['type'] == 'separator':
                self.add_toolbarseparator(e_tb)
                continue

    def add_toolbaritem(self, root, left_macro_id, right_macro_id):
        guid = uuid.uuid4()
        s_item = TPL_TOOLBARITEM.format(guid, left_macro_id, right_macro_id)
        e_item = ET.fromstring(s_item)
        root.append(e_item)

    def add_toolbarseparator(self, root):
        guid = uuid.uuid4()
        s_sep = TPL_TOOLBARSEPARATOR.format(guid)
        e_sep = ET.fromstring(s_sep)
        root.append(e_sep)

    # --------------------------------------------------------------------------
    # add toolbargroups
    # --------------------------------------------------------------------------

    def add_toolbargroups(self, toolbargroups):
        for tbg in toolbargroups:
            self.add_toolbargroup(tbg)

    def add_toolbargroup(self, tbg):
        options = {
            'single_file': 'False',
            'hide_single_tab': 'False',
        }
        guid = uuid.uuid4()
        s_tbg = TPL_TOOLBARGROUP.format(guid, tbg['name'], options)
        e_tbg = ET.fromstring(s_tbg)
        self.root_toolbargroups.append(e_tbg)
        for tb_name in tbg['toolbars']:
            e_tb = self.toolbars[tb_name]
            tb_guid = e_tb.attrib['guid']
            self.add_toolbargroupitem(e_tbg, tb_name, tb_guid)

    def add_toolbargroupitem(self, root, tb_name, tb_guid):
        guid = uuid.uuid4()
        s_item = TPL_TOOLBARGROUPITEM.format(guid, tb_name, tb_guid)
        e_item = ET.fromstring(s_item)
        root.append(e_item)


# def compile_rui(oclass, opath, rui_config='rui_config.json'):
#     """Compile an RUI file from a MacroController class.

#     Parameters
#     ----------
#     oclass : MacroController
#         The controller class.
#     opath : str
#         The module path of the controller class.
#     rui_config : str, optional
#         The path of the configuration file.
#         Default is ``'rui_config.json'``

#     Returns
#     -------
#     None

#     Examples
#     --------
#     .. code-block:: python

#         #

#     """

#     if not oclass.instancename:
#         raise Exception('MacroController instance name not set.')

#     with open(os.path.join(rui_config), 'r') as fp:
#         config = json.load(fp)

#     macros = get_macros(oclass, oclass.instancename)

#     init_script = [
#         '-_RunPythonScript ResetEngine (',
#         'import rhinoscriptsyntax as rs;',
#         'import sys;',
#         'import os;',
#         'path = rs.ToolbarCollectionPath(\'{}\');'.format(oclass.instancename),
#         'path = os.path.dirname(path);',
#         'sys.path.insert(0, path);',
#         'from {} import {};'.format(opath, oclass.__name__),
#         '{} = {}();'.format(oclass.instancename, oclass.__name__),
#         '{}.init();'.format(oclass.instancename),
#         ')'
#     ]

#     update_macro(macros, '{}.init'.format(oclass.instancename), 'script', ''.join(init_script))

#     rui = Rui('./{}.rui'.format(oclass.instancename))

#     rui.init()
#     rui.add_macros(macros)
#     rui.add_menus(config['menus'])
#     rui.add_toolbars(config['toolbars'])
#     rui.add_toolbargroups(config['toolbargroups'])
#     rui.write()


# src/compas_rhino/utilities/__init__.py

"""
********************************************************************************
utilities
********************************************************************************

.. currentmodule:: compas_rhino.utilities


layers
======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    create_layers
    clear_layers
    delete_layers


objects
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    delete_object
    delete_objects
    get_line_coordinates
    get_objects
    get_object_attributes
    set_object_attributes
    get_object_attributes_from_name
    get_object_layers
    get_object_types
    get_object_names
    get_object_name
    get_point_coordinates
    get_polyline_coordinates
    get_polygon_coordinates
    purge_objects
    select_curve
    select_curves
    select_line
    select_lines
    select_mesh
    select_meshes
    select_point
    select_points
    select_polygon
    select_polygons
    select_polyline
    select_polylines
    select_surface
    select_surfaces


drawing
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    draw_labels
    draw_points
    draw_lines
    draw_polylines
    draw_faces
    draw_cylinders
    draw_pipes
    draw_spheres
    draw_mesh


constructors
============

.. autosummary::
    :toctree: generated/
    :nosignatures:

    volmesh_from_polysurfaces

"""
from __future__ import absolute_import

from .document import (
    get_document_basename,
    get_document_filename,
    get_document_extension,
    get_document_filepath,
    get_document_dirname
)
from .layers import (
    create_layers_from_path,
    create_layers_from_paths,
    create_layers_from_dict,
    create_layers,
    clear_layer,
    clear_current_layer,
    clear_layers,
    delete_layers
)
from .objects import (
    find_object,
    get_objects,
    get_object_layers,
    get_object_types,
    get_object_names,
    get_object_name,
    get_object_attributes,
    set_object_attributes,
    get_object_attributes_from_name,
    delete_object,
    delete_objects,
    purge_objects,
    is_curve_line,
    is_curve_polyline,
    is_curve_polygon,
    get_points,
    get_curves,
    get_lines,
    get_polylines,
    get_polygons,
    get_point_coordinates,
    get_line_coordinates,
    get_polyline_coordinates,
    get_polygon_coordinates,
    get_meshes,
    get_mesh_face_vertices,
    get_mesh_vertex_coordinates,
    get_mesh_vertex_colors,
    set_mesh_vertex_colors,
    get_mesh_vertices_and_faces,
    get_mesh_vertex_index,
    get_mesh_face_index,
    get_mesh_edge_index,
    select_object,
    select_objects,
    select_point,
    select_points,
    select_curve,
    select_curves,
    select_line,
    select_lines,
    select_polyline,
    select_polylines,
    select_polygon,
    select_polygons,
    select_surface,
    select_surfaces,
    select_mesh,
    select_meshes
)
from .misc import (
    wait,
    get_tolerance,
    toggle_toolbargroup,
    pick_point,
    browse_for_folder,
    browse_for_file,
    print_display_on,
    display_message,
    display_text,
    display_image,
    display_html,
    update_settings,
    update_named_values,
    screenshot_current_view,
    select_folder,
    select_file,
    unload_modules
)
from .drawing import (
    draw_labels,
    draw_points,
    draw_lines,
    draw_geodesics,
    draw_polylines,
    draw_breps,
    draw_faces,
    draw_cylinders,
    draw_pipes,
    draw_spheres,
    draw_mesh,
    draw_circles
)
from .geometry import uv_points_from_surface
from .constructors import volmesh_from_polysurfaces


__all__ = [
    'get_document_basename',
    'get_document_filename',
    'get_document_extension',
    'get_document_filepath',
    'get_document_dirname',
    'create_layers_from_path',
    'create_layers_from_paths',
    'create_layers_from_dict',
    'create_layers',
    'clear_layer',
    'clear_current_layer',
    'clear_layers',
    'delete_layers',
    'find_object',
    'get_objects',
    'get_object_layers',
    'get_object_types',
    'get_object_names',
    'get_object_name',
    'get_object_attributes',
    'set_object_attributes',
    'get_object_attributes_from_name',
    'delete_object',
    'delete_objects',
    'purge_objects',
    'is_curve_line',
    'is_curve_polyline',
    'is_curve_polygon',
    'get_points',
    'get_curves',
    'get_lines',
    'get_polylines',
    'get_polygons',
    'get_point_coordinates',
    'get_line_coordinates',
    'get_polyline_coordinates',
    'get_polygon_coordinates',
    'get_meshes',
    'get_mesh_face_vertices',
    'get_mesh_vertex_coordinates',
    'get_mesh_vertex_colors',
    'set_mesh_vertex_colors',
    'get_mesh_vertices_and_faces',
    'get_mesh_vertex_index',
    'get_mesh_face_index',
    'get_mesh_edge_index',
    'select_object',
    'select_objects',
    'select_point',
    'select_points',
    'select_curve',
    'select_curves',
    'select_line',
    'select_lines',
    'select_polyline',
    'select_polylines',
    'select_polygon',
    'select_polygons',
    'select_surface',
    'select_surfaces',
    'select_mesh',
    'select_meshes',
    'wait',
    'get_tolerance',
    'toggle_toolbargroup',
    'pick_point',
    'browse_for_folder',
    'browse_for_file',
    'print_display_on',
    'display_message',
    'display_text',
    'display_image',
    'display_html',
    'update_settings',
    'update_named_values',
    'screenshot_current_view',
    'select_folder',
    'select_file',
    'unload_modules',
    'draw_labels',
    'draw_points',
    'draw_lines',
    'draw_geodesics',
    'draw_polylines',
    'draw_breps',
    'draw_faces',
    'draw_cylinders',
    'draw_pipes',
    'draw_spheres',
    'draw_mesh',
    'draw_circles',
    'uv_points_from_surface',
    'volmesh_from_polysurfaces'
]


# src/compas_rhino/utilities/constructors.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.utilities import geometric_key

import Rhino
import scriptcontext as sc


__all__ = ['volmesh_from_polysurfaces']


def volmesh_from_polysurfaces(cls, guids, precision=None):
    """Construct a volumetric mesh from given polysurfaces.

    Parameters
    ----------
    cls : :class:`compas.datastructures.VolMesh`
        The class of volmesh.
    guids : sequence[str or System.Guid]
        The *globally unique identifiers* of the polysurfaces.
    precision: string
        Precision of the polysurface connectivity.

    Returns
    -------
    :class:`compas.datastructures.Volmesh`
        The volumetric mesh object.

    Notes
    -----
    Essentially, this function does the following:

    * find each of the polysurfaces and check if they have a boundary representation (b-rep)
    * convert to b-rep and extract the edge loops
    * make a face of each loop by referring to vertices using their geometric keys
    * add a cell per brep
    * and add the faces of a brep to the cell
    * create a volmesh from the found vertices and cells

    """
    gkey_xyz = {}
    cells = []
    for guid in guids:
        cell = []
        obj = sc.doc.Objects.Find(guid)
        if not obj.Geometry.HasBrepForm:
            continue
        brep = Rhino.Geometry.Brep.TryConvertBrep(obj.Geometry)
        for loop in brep.Loops:
            curve = loop.To3dCurve()
            segments = curve.Explode()
            face = []
            sp = segments[0].PointAtStart
            ep = segments[0].PointAtEnd
            sp_gkey = geometric_key(sp, precision)
            ep_gkey = geometric_key(ep, precision)
            gkey_xyz[sp_gkey] = sp
            gkey_xyz[ep_gkey] = ep
            face.append(sp_gkey)
            face.append(ep_gkey)
            for segment in segments[1:-1]:
                ep = segment.PointAtEnd
                ep_gkey = geometric_key(ep, precision)
                face.append(ep_gkey)
                gkey_xyz[ep_gkey] = ep
            cell.append(face)
        cells.append(cell)
    gkey_index = dict((gkey, index) for index, gkey in enumerate(gkey_xyz))
    vertices = [list(xyz) for gkey, xyz in gkey_xyz.items()]
    cells = [[[gkey_index[gkey] for gkey in face] for face in cell] for cell in cells]
    return cls.from_vertices_and_cells(vertices, cells)


# src/compas_rhino/utilities/document.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import os
import rhinoscriptsyntax as rs


__all__ = [
    'get_document_basename',
    'get_document_filename',
    'get_document_extension',
    'get_document_filepath',
    'get_document_dirname'
]


def get_document_basename():
    """Get the name of the Rhino document (including the extension).

    Returns
    -------
    str or None
        The name of the document or None if the document is still "Untitled".

    """
    return rs.DocumentName()


def get_document_filename():
    """Get the name of the Rhino document without the file extension.

    Returns
    -------
    str or None
        The name of the document or None if the document is still "Untitled".

    """
    basename = get_document_basename()
    if not basename:
        return None
    return os.path.splitext(basename)[0]


def get_document_extension():
    """Get the extension of the Rhino document (including the dot separator).

    Returns
    -------
    str or None
        The extension of the document or None if the document is still "Untitled".

    """
    basename = get_document_basename()
    if not basename:
        return None
    return os.path.splitext(basename)[1]


def get_document_filepath():
    """Get the full path to the Rhino document.

    Returns
    -------
    str or None
        The path to the document or None if the document is still "Untitled".

    """
    return rs.DocumentPath()


def get_document_dirname():
    """Get the name of the directory containing the Rhino document.

    Returns
    -------
    str or None
        The name of the directory or None if the document is still "Untitled".

    """
    filepath = get_document_filepath()
    if not filepath:
        return None
    return os.path.dirname(filepath)


# src/compas_rhino/utilities/drawing.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from functools import wraps

import compas_rhino

from compas.geometry import centroid_polygon
from compas.utilities import pairwise

from compas_rhino.utilities import create_layers_from_path
from compas_rhino.utilities import clear_layer
from compas_rhino.utilities import clear_current_layer

import System

from System.Collections.Generic import List
from System.Drawing.Color import FromArgb
from System.Enum import ToObject

import rhinoscriptsyntax as rs
import scriptcontext as sc

from Rhino.Geometry import Point3d
from Rhino.Geometry import Vector3d
from Rhino.Geometry import Polyline
from Rhino.Geometry import PolylineCurve
from Rhino.Geometry import GeometryBase
from Rhino.Geometry import Brep
from Rhino.Geometry import Cylinder
from Rhino.Geometry import Circle
from Rhino.Geometry import Plane
from Rhino.Geometry import PipeCapMode
from Rhino.Geometry import Curve
from Rhino.Geometry import Sphere
from Rhino.Geometry import TextDot
from Rhino.Geometry import Mesh as RhinoMesh

try:
    from Rhino.Geometry import MeshNgon
except ImportError:
    MeshNgon = False

from Rhino.DocObjects.ObjectColorSource import ColorFromObject
from Rhino.DocObjects.ObjectColorSource import ColorFromLayer
from Rhino.DocObjects.ObjectDecoration import EndArrowhead
from Rhino.DocObjects.ObjectDecoration import StartArrowhead
from Rhino.DocObjects.ObjectPlotWeightSource import PlotWeightFromObject

find_object = sc.doc.Objects.Find
add_point = sc.doc.Objects.AddPoint
add_line = sc.doc.Objects.AddLine
add_dot = sc.doc.Objects.AddTextDot
add_curve = sc.doc.Objects.AddCurve
add_polyline = sc.doc.Objects.AddPolyline
add_brep = sc.doc.Objects.AddBrep
add_sphere = sc.doc.Objects.AddSphere
add_mesh = sc.doc.Objects.AddMesh
add_circle = sc.doc.Objects.AddCircle

TOL = sc.doc.ModelAbsoluteTolerance

try:
    find_layer_by_fullpath = sc.doc.Layers.FindByFullPath
except SystemError:
    find_layer_by_fullpath = None


__all__ = [
    'draw_labels',
    'draw_points',
    'draw_lines',
    'draw_geodesics',
    'draw_polylines',
    'draw_breps',
    'draw_faces',
    'draw_cylinders',
    'draw_pipes',
    'draw_spheres',
    'draw_mesh',
    'draw_circles',
]


def wrap_drawfunc(f):
    """Wraps all ``draw_`` functions with support for recurring keyword arguments."""
    @wraps(f)
    def wrapper(*args, **kwargs):
        layer = kwargs.get('layer', None)
        clear = kwargs.get('clear', False)
        redraw = kwargs.get('redraw', False)
        if layer:
            if not rs.IsLayer(layer):
                create_layers_from_path(layer)
            previous = rs.CurrentLayer(layer)
        if clear:
            if not layer:
                clear_current_layer()
            else:
                clear_layer(layer)
        rs.EnableRedraw(False)
        res = f(*args, **kwargs)
        if redraw:
            rs.EnableRedraw(True)
        if layer:
            rs.CurrentLayer(previous)
        return res
    return wrapper


@wrap_drawfunc
def draw_labels(labels, **kwargs):
    """Draw labels as text dots and optionally set individual font, fontsize, name, layer and color.

    Parameters
    ----------
    labels : list[dict]
        A list of labels dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A label dict has the following schema:

    .. code-block:: python

        Schema({
            'pos': And(list, lambda x: len(x) == 3),
            'text': And(str, len),
            Optional('name', default=''): str,
            Optional('color', default=None): (lambda x: len(x) == 3 and all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
            Optional('fontsize', default=10): Or(int, float),
            Optional('font', default="Arial Regular"): str
        })

    """
    guids = []
    for label in iter(labels):
        pos = label['pos']
        text = label['text']
        name = label.get('name', '')
        color = label.get('color', None)
        layer = label.get('layer')
        size = label.get('fontsize', 10)
        font = label.get('font', 'Arial Regular')
        dot = TextDot(str(text), Point3d(*pos))
        dot.FontHeight = size
        dot.FontFace = font
        guid = add_dot(dot)
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_points(points, **kwargs):
    """Draw points and optionally set individual name, layer, and color properties.

    Parameters
    ----------
    labels : list[dict]
        A list of point dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A point dict has the following schema:

    .. code-block:: python

        Schema({
            'pos': And(list, lambda x: len(x) == 3),
            Optional('name', default=''): str,
            Optional('color', default=None): (lambda x: len(x) == 3 and all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str
        })

    """
    guids = []
    for p in iter(points):
        pos = p['pos']
        name = p.get('name', '')
        color = p.get('color')
        layer = p.get('layer')
        guid = add_point(Point3d(*pos))
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_lines(lines, **kwargs):
    """Draw lines and optionally set individual name, color, arrow, layer, and
    width properties.

    Parameters
    ----------
    labels : list[dict]
        A list of line dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A line dict has the following schema:

    .. code-block:: python

        Schema({
            'start': And(list, lambda x: len(x) == 3),
            'end': And(list, lambda x: len(x) == 3),
            Optional('name', default=''): str,
            Optional('color', default=None): (lambda x: len(x) == 3 and all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
            Optional('arrow', default=None): str,
            Optional('width', default=None): Or(int, float),
        })

    """
    guids = []
    for line in iter(lines):
        sp = line['start']
        ep = line['end']
        name = line.get('name', '')
        color = line.get('color')
        arrow = line.get('arrow')
        layer = line.get('layer')
        width = line.get('width')
        guid = add_line(Point3d(*sp), Point3d(*ep))
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if arrow == 'end':
            attr.ObjectDecoration = EndArrowhead
        if arrow == 'start':
            attr.ObjectDecoration = StartArrowhead
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        if width:
            attr.PlotWeight = width
            attr.PlotWeightSource = PlotWeightFromObject
        attr.Name = name
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_geodesics(geodesics, **kwargs):
    """Draw geodesic lines on specified surfaces, and optionally set individual
    name, color, arrow, and layer properties.

    Parameters
    ----------
    labels : list[dict]
        A list of geodesic dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A geodesic dict has the following schema:

    .. code-block:: python

        Schema({
            'start': And(list, lambda x: len(x) == 3),
            'end': And(list, lambda x: len(x) == 3),
            'srf': Or(str, System.Guid),
            Optional('name', default=''): str,
            Optional('color', default=None): (lambda x: len(x) == 3 and all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
            Optional('arrow', default=None): str,
        })

    """
    guids = []
    for g in iter(geodesics):
        sp = g['start']
        ep = g['end']
        srf = g['srf']
        name = g.get('name', '')
        color = g.get('color')
        arrow = g.get('arrow')
        layer = g.get('layer')
        # replace this by a proper rhinocommon call
        guid = rs.ShortPath(srf, Point3d(*sp), Point3d(*ep))
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if arrow == 'end':
            attr.ObjectDecoration = EndArrowhead
        if arrow == 'start':
            attr.ObjectDecoration = StartArrowhead
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_polylines(polylines, **kwargs):
    """Draw polylines, and optionally set individual name, color, arrow, and
    layer properties.

    Parameters
    ----------
    labels : list[dict]
        A list of polyline dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A polyline dict has the following schema:

    .. code-block:: python

        Schema({
            'points': And(list, lambda x: all(len(point) == 3 for point in x),
            Optional('name', default=''): str,
            Optional('color', default=None): (lambda x: len(x) == 3 and all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
            Optional('arrow', default=None): str
        })

    """
    guids = []
    for p in iter(polylines):
        points = p['points']
        name = p.get('name', '')
        color = p.get('color')
        arrow = p.get('arrow')
        layer = p.get('layer')
        poly = Polyline([Point3d(*xyz) for xyz in points])
        poly.DeleteShortSegments(TOL)
        guid = add_polyline(poly)
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if arrow == 'end':
            attr.ObjectDecoration = EndArrowhead
        if arrow == 'start':
            attr.ObjectDecoration = StartArrowhead
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_breps(faces, u=10, v=10, join=False, **kwargs):
    """Draw polygonal faces as Breps, and optionally set individual name, color,
    and layer properties.

    Parameters
    ----------
    faces : list[dict]
        A list of brep dictionaries.
        See Notes, for more information about the structure of the dict.
    u : int, optional
        Number of spans in the U direction.
    v : int, optional
        Number of spans in the V direction.
    join : bool, optional
        If True, join the individual faces as polysurfaces

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A brep dict has the following schema:

    .. code-block:: python

        Schema({
            'points': And(list, lambda x: len(x) > 3 and all(len(point) == 3 for point in x),
            Optional('name', default=''): str,
            Optional('color', default=None): (lambda x: len(x) == 3 and all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
        })

    Examples
    --------
    Using a compas Mesh as an example:

    >>> from compas.datastructures import Mesh
    >>> from compas.geometry import Box, Frame
    >>> from compas_rhino.utilities import draw_breps
    >>> box = Box(Frame.worldXY(), 1.0, 2.0, 3.0)
    >>> mesh = Mesh.from_shape(box)

    Draw convert each mesh face to brep dict schema:

    >>> vertices = mesh.vertices_attributes('xyz')
    >>> breps = [{'points': mesh.face_coordinates(face)} for face in mesh.faces()]

    Draw brep faces as one joined brep.

    >>> guids = draw_breps(breps, join=True)

    """
    breps = []
    for f in iter(faces):
        points = f['points']
        name = f.get('name', '')
        color = f.get('color')
        layer = f.get('layer')
        corners = [Point3d(*point) for point in points + points[:1]]
        pcurve = PolylineCurve(corners)
        geo = List[GeometryBase](1)
        geo.Add(pcurve)
        p = len(points)
        if p == 3:
            brep = Brep.CreateFromCornerPoints(Point3d(*points[0]),
                                               Point3d(*points[1]),
                                               Point3d(*points[2]),
                                               TOL)
        elif p == 4:
            brep = Brep.CreateFromCornerPoints(Point3d(*points[0]),
                                               Point3d(*points[1]),
                                               Point3d(*points[2]),
                                               Point3d(*points[3]),
                                               TOL)
        else:
            brep = Brep.CreatePatch(geo, u, v, TOL)
        if brep:
            breps.append(brep)

    if join:
        breps = Brep.JoinBreps(breps, TOL)

    guids = []
    for brep in breps:
        guid = add_brep(brep)
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        attr.WireDensity = -1
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_cylinders(cylinders, cap=False, **kwargs):
    """Draw cylinders and optionally set individual name, color, and layer properties.

    Parameters
    ----------
    cylinders : list[dict]
        A list of cylinder dictionaries.
        See Notes, for more information about the structure of the dict.
    cap : bool, optional
        If True, add caps.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A cylinder dict has the following schema:

    .. code-block:: python

        Schema({
            'start': And(list, lambda x: len(x) == 3),
            'end': And(list, lambda x: len(x) == 3),
            'radius': And(Or(int, float), lambda x: x > 0.0),
            Optional('name', default=''): str,
            Optional('color', default=None): (lambda x: len(x) == 3 and all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
        })

    """
    guids = []
    for c in iter(cylinders):
        start = c['start']
        end = c['end']
        radius = c['radius']
        name = c.get('name', '')
        color = c.get('color')
        layer = c.get('layer')
        if radius < TOL:
            continue
        base = Point3d(*start)
        normal = Point3d(*end) - base
        height = normal.Length
        if height < TOL:
            continue
        plane = Plane(base, normal)
        circle = Circle(plane, radius)
        cylinder = Cylinder(circle, height)
        brep = cylinder.ToBrep(cap, cap)
        if not brep:
            continue
        guid = add_brep(brep)
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        attr.WireDensity = -1
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_pipes(pipes, cap=2, fit=1.0, **kwargs):
    """Draw pipes and optionally set individual name, color, and layer properties.

    Parameters
    ----------
    pipes : list[dict]
        A list of pipe dictionaries.
        See Notes, for more information about the structure of the dict.

    Other Parameters
    ----------------
    cap : {0, 1, 2}, optional
    fit : float, optional

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A pipe dict has the following schema:

    .. code-block:: python

        Schema({
            'points': And(list, lambda x: all(len(y) == 3 for y in x)),
            'radius': And(Or(int, float), lambda x: x > 0.0),
            Optional('name', default=''): str,
            Optional('color', default=None): And(lambda x: len(x) == 3, all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
        })

    """
    guids = []
    abs_tol = TOL
    ang_tol = sc.doc.ModelAngleToleranceRadians
    for p in pipes:
        points = p['points']
        radius = p['radius']
        name = p.get('name', '')
        color = p.get('color')
        layer = p.get('layer')
        params = [0.0, 1.0]
        cap = ToObject(PipeCapMode, cap)
        if type(radius) in (int, float):
            radius = [radius] * 2
        radius = [float(r) for r in radius]
        rail = Curve.CreateControlPointCurve([Point3d(*xyz) for xyz in points])
        breps = Brep.CreatePipe(rail, params, radius, 1, cap, fit, abs_tol, ang_tol)
        temp = [add_brep(brep) for brep in breps]
        for guid in temp:
            if not guid:
                continue
            obj = find_object(guid)
            if not obj:
                continue
            attr = obj.Attributes
            if color:
                attr.ObjectColor = FromArgb(*color)
                attr.ColorSource = ColorFromObject
            else:
                attr.ColorSource = ColorFromLayer
            if layer and find_layer_by_fullpath:
                index = find_layer_by_fullpath(layer, True)
                if index >= 0:
                    attr.LayerIndex = index
            attr.Name = name
            attr.WireDensity = -1
            obj.CommitChanges()
            guids.append(guid)
    return guids


@wrap_drawfunc
def draw_spheres(spheres, **kwargs):
    """Draw spheres and optionally set individual name, color, and layer properties.

    Parameters
    ----------
    spheres : list[dict]
        A list of sphere dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A sphere dict has the following schema:

    .. code-block:: python

        Schema({
            'pos': And(list, lambda x: len(x) == 3),
            'radius': And(Or(int, float), lambda x: x > 0.0),
            Optional('name', default=''): str,
            Optional('color', default=None): And(lambda x: len(x) == 3, all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str,
        })

    """
    guids = []
    for s in iter(spheres):
        pos = s['pos']
        radius = s['radius']
        name = s.get('name', '')
        color = s.get('color')
        layer = s.get('layer')
        sphere = Sphere(Point3d(*pos), radius)
        guid = add_sphere(sphere)
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        attr.WireDensity = -1
        obj.CommitChanges()
        guids.append(guid)
    return guids


@wrap_drawfunc
def draw_mesh(vertices, faces, name=None, color=None, disjoint=False, **kwargs):
    """Draw a mesh and optionally set individual name, color, and layer properties.

    Parameters
    ----------
    vertices : list[[float, float, float] or :class:`compas.geometry.Point`]
        A list of point locations.
    faces : list[list[int]]
        A list of faces as lists of indices into `vertices`.
    name : str, optional
    color : tuple[[int, int, int]], optional
    disjoint : bool, optional
        If True, draw the mesh with disjoint faces.

    Returns
    -------
    System.Guid

    """
    mesh = RhinoMesh()
    if disjoint:
        for face in faces:
            f = len(face)
            if f < 3:
                continue
            if f == 3:
                a = mesh.Vertices.Add(* vertices[face[0]])
                b = mesh.Vertices.Add(* vertices[face[1]])
                c = mesh.Vertices.Add(* vertices[face[2]])
                mesh.Faces.AddFace(a, b, c)
            elif f == 4:
                a = mesh.Vertices.Add(* vertices[face[0]])
                b = mesh.Vertices.Add(* vertices[face[1]])
                c = mesh.Vertices.Add(* vertices[face[2]])
                d = mesh.Vertices.Add(* vertices[face[3]])
                mesh.Faces.AddFace(a, b, c, d)
            else:
                if MeshNgon:
                    points = [vertices[vertex] for vertex in face]
                    centroid = centroid_polygon(points)
                    indices = []
                    for point in points:
                        indices.append(mesh.Vertices.Add(* point))
                    c = mesh.Vertices.Add(* centroid)
                    facets = []
                    for i, j in pairwise(indices + indices[:1]):
                        facets.append(mesh.Faces.AddFace(i, j, c))
                    ngon = MeshNgon.Create(indices, facets)
                    mesh.Ngons.AddNgon(ngon)
    else:
        for x, y, z in vertices:
            mesh.Vertices.Add(x, y, z)
        for face in faces:
            f = len(face)
            if f < 3:
                continue
            if f == 3:
                mesh.Faces.AddFace(*face)
            elif f == 4:
                mesh.Faces.AddFace(*face)
            else:
                if MeshNgon:
                    centroid = centroid_polygon([vertices[index] for index in face])
                    c = mesh.Vertices.Add(* centroid)
                    facets = []
                    for i, j in pairwise(face + face[:1]):
                        facets.append(mesh.Faces.AddFace(i, j, c))
                    ngon = MeshNgon.Create(face, facets)
                    mesh.Ngons.AddNgon(ngon)

    mesh.Normals.ComputeNormals()
    mesh.Compact()

    guid = add_mesh(mesh)

    if guid != System.Guid.Empty:
        obj = find_object(guid)
        if obj:
            attr = obj.Attributes
            if color:
                attr.ObjectColor = FromArgb(*color)
                attr.ColorSource = ColorFromObject
            else:
                attr.ColorSource = ColorFromLayer
            if name:
                attr.Name = name
            obj.CommitChanges()
        return guid


@wrap_drawfunc
def draw_faces(faces, **kwargs):
    """Draw faces as individual meshes and optionally set individual name, color, and layer properties.

    Parameters
    ----------
    faces : list[dict]
        A list of face dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A face dict has the following schema:

    .. code-block:: python

        Schema({
            'points': And(len, lambda x: all(len(y) == 3 for y in x)),
            Optional('name', default=''): str,
            Optional('color', default=None): And(lambda x: len(x) == 3, all(0 <= y <= 255 for y in x)),
            Optional('vertexcolors', default=None): And(len, lambda x: all(0 <= y <= 255 for y in x)),
        })

    """
    guids = []
    for face in iter(faces):
        points = face['points'][:]
        name = face.get('name')
        color = face.get('color')
        vertexcolors = face.get('vertexcolors')

        v = len(points)

        if v < 3:
            continue
        elif v == 3:
            mfaces = [[0, 1, 2, 2]]
        elif v == 4:
            mfaces = [[0, 1, 2, 3]]
        else:
            mfaces = [list(range(v))]

        guid = draw_mesh(points, mfaces, color=color, name=name, clear=False, redraw=False, layer=None)

        if vertexcolors:
            try:
                compas_rhino.set_mesh_vertex_colors(guid, vertexcolors)
            except Exception:
                pass

        guids.append(guid)

    return guids


def _face_to_max_quad(points, face):
    faces = []
    c = len(points)
    points.append(centroid_polygon(points))
    for i in range(-1, len(face) - 1):
        a = face[i]
        b = face[i + 1]
        faces.append([c, a, b, b])
    return faces


@wrap_drawfunc
def draw_circles(circles, **kwargs):
    """Draw circles and optionally set individual name, color, and layer properties.

    Parameters
    ----------
    circles : list[dict]
        A list of circle dictionaries.
        See Notes, for more information about the structure of the dict.

    Returns
    -------
    list[System.Guid]

    Notes
    -----
    A circle dict has the following schema:

    .. code-block:: python

        Schema({
            'plane': lambda x: len(x[0]) == 3 and len(x[1]) == 3,
            'radius': And(Or(int, float), lambda x: x > 0),
            Optional('name', default=''): str,
            Optional('color', default=None): And(lambda x: len(x) == 3, all(0 <= y <= 255 for y in x)),
            Optional('layer', default=None): str
        })

    """
    guids = []
    for data in iter(circles):
        point, normal = data['plane']
        radius = data['radius']
        name = data.get('name', '')
        color = data.get('color')
        layer = data.get('layer')
        circle = Circle(Plane(Point3d(*point), Vector3d(*normal)), radius)
        guid = add_circle(circle)
        if not guid:
            continue
        obj = find_object(guid)
        if not obj:
            continue
        attr = obj.Attributes
        if color:
            attr.ObjectColor = FromArgb(*color)
            attr.ColorSource = ColorFromObject
        else:
            attr.ColorSource = ColorFromLayer
        if layer and find_layer_by_fullpath:
            index = find_layer_by_fullpath(layer, True)
            if index >= 0:
                attr.LayerIndex = index
        attr.Name = name
        attr.WireDensity = -1
        obj.CommitChanges()
        guids.append(guid)
    return guids


# src/compas_rhino/utilities/geometry.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import rhinoscriptsyntax as rs


__all__ = [
    'uv_points_from_surface',
]


def uv_points_from_surface(srf, u_div, v_div):
    """Creates a nested uv point list from a surface.

    Parameters
    ----------
    srf : System.Guid
        The surface identifier.
    u_div : int
        Number of poinst in u direction
    v_div : int
        Number of poinst in v direction

    Returns
    -------
    list[list[:rhino:`Rhino.Geometry.Point3d`]]
        Points for every uv division.

    """
    u_domain = rs.SurfaceDomain(srf, 0)
    v_domain = rs.SurfaceDomain(srf, 1)
    u_step = (u_domain[1] - u_domain[0]) / (u_div - 1)
    v_step = (v_domain[1] - v_domain[0]) / (v_div - 1)

    uv_points = [[None for _ in range(v_div)] for _ in range(u_div)]

    for u in range(u_div):
        for v in range(v_div):
            uv = (u_domain[0] + u_step * u, v_domain[0] + v_step * v)
            uv_points[u][v] = rs.EvaluateSurface(srf, uv[0], uv[1])

    return uv_points


# src/compas_rhino/utilities/layers.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from collections import deque

import rhinoscriptsyntax as rs
import scriptcontext as sc

find_object = sc.doc.Objects.Find

try:
    purge_object = sc.doc.Objects.Purge
except AttributeError:
    purge_object = None


__all__ = [
    'create_layers_from_path',
    'create_layers_from_paths',
    'create_layers_from_dict',
    'create_layers',
    'clear_layer',
    'clear_current_layer',
    'clear_layers',
    'delete_layers',
]


# ==============================================================================
# helpers
# ==============================================================================


def show_hidden_objects_on_layer(name):
    rs.ShowObjects([guid for guid in rs.HiddenObjects() if rs.ObjectLayer(guid) == name])


def find_objects_on_layer(name, include_hidden=True, include_children=True):
    if include_hidden:
        show_hidden_objects_on_layer(name)
    to_delete = rs.ObjectsByLayer(name)
    if include_children:
        to_visit = deque(rs.LayerChildren(name))
        while to_visit:
            name = to_visit.popleft()
            if include_hidden:
                show_hidden_objects_on_layer(name)
            to_delete += rs.ObjectsByLayer(name)
            if rs.LayerChildCount(name):
                to_visit.extend(rs.LayerChildren(name))
    return to_delete


def delete_objects_on_layer(name, include_hidden=True, include_children=False, purge=True):
    guids = find_objects_on_layer(name, include_hidden, include_children)
    if purge and purge_object:
        rs.EnableRedraw(False)
        for guid in guids:
            obj = find_object(guid)
            if not obj:
                continue
            purge_object(obj.RuntimeSerialNumber)
        rs.EnableRedraw(True)
    else:
        rs.DeleteObjects(guids)


# ==============================================================================
# create
# ==============================================================================

def create_layers_from_path(path, separator='::'):
    """Create a nested layer structure from a hierarchical path string.

    Parameters
    ----------
    path : str
        The path string.
    separator : str, optional
        Separator between components of the layer path.

    Returns
    -------
    None

    Examples
    --------
    The following snippet will created 3 nested layers,
    with "COMPAS" at the root level, "Datastructures" at the first nested level, and "Mesh" at the deepest level.

    * COMPAS
      * Datastructures
        * Mesh

    .. code-block:: python

        create_layers_from_path("COMPAS::Datastructures::Mesh")

    """
    names = path.split(separator)
    parent = None
    for name in names:
        if parent:
            name = parent + separator + name
        if not rs.IsLayer(name):
            rs.AddLayer(name)
        parent = name


def create_layers_from_paths(names, separator='::'):
    """Create nested layers from a lst of hierarchical path strings.

    Parameters
    ----------
    names : list[str]
        The path strings of the nested layer structures.
    separator : str, optional
        Separator between components of the layer path.

    Returns
    -------
    None

    Examples
    --------
    The following snippet will created 2 nested layer structures:

    * COMPAS
      * Datastructures
        * Mesh
        * Network
      * Geometry
        * Point
        * Vector

    .. code-block:: python

        create_layers_from_paths([
            "COMPAS::Datastructures::Mesh",
            "COMPAS::Datastructures::Network",
            "COMPAS::Geometry::Point",
            "COMPAS::Geometry::Vector",
        ])

    """
    for name in names:
        create_layers_from_path(name, separator=separator)


def create_layers_from_dict(layers):
    """Create Rhino layers from a dictionary.

    Parameters
    ----------
    layers : dict[str, dict]
        A dictionary of nested layer definitions.
        The keys of the dict are the layer names.
        The corresponding values define optional layer properties and nested layers.

    Returns
    -------
    None

    Examples
    --------
    .. code-block:: python

        layers = {'COMPAS', {'layers': {
            'Datastructures': {'color': (255, 0, 0), 'layers': {
                'Mesh': {},
                'Network': {}
            }},
            'Geometry': {'color': (0, 0, 255),'layers': {
                'Point': {},
                'Vector': {}
            }},
        }}}

        create_layers_from_dict(layers)

    """
    def recurse(layers, parent=None):
        for name in layers:
            if not name:
                continue
            fullname = name
            if parent:
                fullname = parent + '::' + name
            try:
                attr = layers[name]
            except TypeError:
                attr = {}
            attr = attr or {}
            color = attr.get('color', (0, 0, 0))
            visible = attr.get('visible', True)
            locked = attr.get('locked', False)
            if not rs.IsLayer(fullname):
                rs.AddLayer(fullname, color, visible, locked)
            if 'layers' in attr:
                recurse(attr['layers'], fullname)
    rs.EnableRedraw(False)
    recurse(layers)
    rs.EnableRedraw(True)


create_layers = create_layers_from_dict


# ==============================================================================
# clear
# ==============================================================================

def clear_layer(name, include_hidden=True, include_children=True, purge=True):
    """Delete all objects of a layer.

    Parameters
    ----------
    name : str
        The full, hierarchical name of the layer.
    include_hidden : bool, optional
        If True, include all hidden objects.
    include_children : bool, optional
        If True, include the objects of child layers.
    purge : bool, optional
        If True, purge history after deleting.

    Returns
    -------
    None

    """
    if not rs.IsLayer(name):
        return
    guids = find_objects_on_layer(name, include_hidden, include_children)
    rs.EnableRedraw(False)
    if purge and purge_object:
        for guid in guids:
            obj = find_object(guid)
            if not obj:
                continue
            purge_object(obj.RuntimeSerialNumber)
    else:
        rs.DeleteObjects(guids)
    rs.EnableRedraw(True)


def clear_current_layer():
    """Delete all objects from the current layer.

    Returns
    -------
    None

    """
    layer = rs.CurrentLayer()
    clear_layer(layer)


def clear_layers(layers, include_children=True, include_hidden=True, purge=True):
    """Delete the objects of the specified layers.

    Parameters
    ----------
    layers : list[str]
        A list of layer names as fully qualified hierarchical paths.
    include_hidden : bool, optional
        If True, include all hidden objects.
    include_children : bool, optional
        If True, include the objects of child layers.
    purge : bool, optional
        If True, purge history after deleting.

    Returns
    -------
    None

    """
    rs.EnableRedraw(False)
    to_delete = []
    for name in layers:
        if rs.IsLayer(name):
            to_delete += find_objects_on_layer(name, include_hidden, include_children)
    if purge and purge_object:
        for guid in to_delete:
            obj = find_object(guid)
            if not obj:
                continue
            purge_object(obj.RuntimeSerialNumber)
    else:
        rs.DeleteObjects(to_delete)
    rs.EnableRedraw(True)


# ==============================================================================
# delete
# ==============================================================================

def delete_layers(layers):
    """Delete layers and all contained objects.

    Parameters
    ----------
    layers : dict or list[str]
        When given as a list the list elements should be name of layers given
        with ``"::"`` as a separator between hierarchies.
        When provided as a dictionary, keys represent layer names, and values are dictionaries defining optional nested layers.

    Returns
    -------
    None

    Examples
    --------
    .. code-block:: python

        layers = {'COMPAS': {'layers': {'Datastructures': {'layers': {'Mesh': {}, 'Network': {}}}}}}

        create_layers(layers)

        delete_layers(['COMPAS::Datastructures::Network'])
        delete_layers({'COMPAS': {'layers': {'Datastructures': {'layers': {'Mesh': {}}}}}})

    """
    to_delete = []

    def recurse(layers, parent=None):
        for name in layers:
            if not name:
                continue
            fullname = name
            if parent:
                fullname = parent + '::' + name
            try:
                attr = layers[name]
            except TypeError:
                attr = {}
            if 'layers' in attr:
                recurse(attr['layers'], fullname)
            to_delete.append(fullname)

    rs.EnableRedraw(False)
    recurse(layers)

    for layer in to_delete:
        if rs.IsLayer(layer):
            if rs.IsLayerCurrent(layer):
                print("Can't delete {} as it is the current layer".format(layer))
                continue

            rs.PurgeLayer(layer)

    rs.EnableRedraw(True)


# src/compas_rhino/utilities/misc.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

try:
    basestring
except NameError:
    basestring = str

import os
import sys
import ast

from compas_rhino.forms import TextForm
from compas_rhino.forms import ImageForm

import System

import rhinoscriptsyntax as rs
import Rhino
import clr

clr.AddReference('Rhino.UI')
import Rhino.UI  # noqa: E402
from Rhino.UI.Dialogs import ShowMessageBox  # noqa: E402

try:
    from compas_rhino.forms import PropertyListForm
except ImportError:
    from Rhino.UI.Dialogs import ShowPropertyListBox


__all__ = [
    'wait',
    'get_tolerance',
    'toggle_toolbargroup',
    'pick_point',
    'browse_for_folder',
    'browse_for_file',
    'print_display_on',
    'display_message',
    'display_text',
    'display_image',
    'display_html',
    'update_settings',
    'update_named_values',
    'screenshot_current_view',
    'select_folder',
    'select_file',
    'unload_modules',
]


# ==============================================================================
# Truly miscellaneous :)
# ==============================================================================


def screenshot_current_view(path,
                            width=1920,
                            height=1080,
                            scale=1,
                            draw_grid=False,
                            draw_world_axes=False,
                            draw_cplane_axes=False,
                            background=False):
    """Take a screenshot of the current view.

    Parameters
    ----------
    path : str
        The filepath for saving the screenshot.

    Other Parameters
    ----------------
    width : int, optional
    height : int, optional
    scale : float, optional
    draw_grid : bool, optional
    draw_world_axes : bool, optional
    draw_cplane_axes : bool, optional
    background : bool, optional

    Returns
    -------
    bool
        True if the command was successful.
        False otherwise.

    """
    properties = [draw_grid, draw_world_axes, draw_cplane_axes, background]
    properties = ["Yes" if item else "No" for item in properties]
    scale = max(1, scale)  # the rhino command requires a scale > 1
    rs.EnableRedraw(True)
    rs.Sleep(0)
    result = rs.Command("-_ViewCaptureToFile \"" + os.path.abspath(path) + "\""
                        " Width=" + str(width) +
                        " Height=" + str(height) +
                        " Scale=" + str(scale) +
                        " DrawGrid=" + properties[0] +
                        " DrawWorldAxes=" + properties[1] +
                        " DrawCPlaneAxes=" + properties[2] +
                        " TransparentBackground=" + properties[3] +
                        " _enter", False)
    rs.EnableRedraw(False)
    return result


def wait():
    """Make Rhino wait to prevent the spinning wheel from appearing.

    """
    return Rhino.RhinoApp.Wait()


def get_tolerance():
    """Get the absolute tolerance.

    Returns
    -------
    float
        The tolerance.

    """
    return rs.UnitAbsoluteTolerance()


def toggle_toolbargroup(rui, group):
    if not os.path.exists(rui) or not os.path.isfile(rui):
        return
    collection = rs.IsToolbarCollection(rui)
    if not collection:
        collection = rs.OpenToolbarCollection(rui)
        if rs.IsToolbar(collection, group, True):
            rs.ShowToolbar(collection, group)
    else:
        if rs.IsToolbar(collection, group, True):
            if rs.IsToolbarVisible(collection, group):
                rs.HideToolbar(collection, group)
            else:
                rs.ShowToolbar(collection, group)


def pick_point(message='Pick a point.'):
    point = rs.GetPoint(message)
    if point:
        return list(point)
    return None


# ==============================================================================
# File system
# ==============================================================================


def browse_for_folder(message=None, default=None):
    return rs.BrowseForFolder(folder=default, message=message, title='compas')


select_folder = browse_for_folder


def browse_for_file(title=None, folder=None, filter=None):
    if filter == 'json':
        filter = 'JSON files (*.json)|*.json||'
    elif filter == 'obj':
        filter = 'OBJ files (*.obj)|*.obj||'
    elif filter == 'fofin':
        filter = 'FOFIN session files (*.fofin)|*.fofin||'
    else:
        pass
    return rs.OpenFileName(title, filter=filter, folder=folder)


select_file = browse_for_file


# ==============================================================================
# Display
# ==============================================================================


def print_display_on(on=True):
    if on:
        rs.Command('_PrintDisplay State On Color Display Thickness 1 _Enter')
    else:
        rs.Command('_PrintDisplay State Off _Enter')


def display_message(message):
    return ShowMessageBox(message, 'Message')


def display_text(text, title='Text', width=800, height=600):
    if isinstance(text, (list, tuple)):
        text = '{0}'.format(System.Environment.NewLine).join(text)
    form = TextForm(text, title, width, height)
    return form.show()


def display_image(image, title='Image', width=800, height=600):
    form = ImageForm(image, title, width, height)
    return form.show()


def display_html():
    raise NotImplementedError


# ==============================================================================
# Settings and attributes
# ==============================================================================


def update_named_values(names, values, message='', title='Update named values', evaluate=False):
    try:
        dialog = PropertyListForm(names, values)
    except Exception:
        values = ShowPropertyListBox(message, title, names, values)
    else:
        if dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow):
            values = dialog.values
        else:
            values = None
    if evaluate:
        if values:
            values = list(values)
            for i in range(len(values)):
                value = values[i]
                try:
                    value = ast.literal_eval(value)
                except (TypeError, ValueError, SyntaxError):
                    pass
                values[i] = value
    return values


def update_settings(settings, message='', title='Update settings'):
    names = sorted(settings.keys())
    values = [str(settings[name]) for name in names]
    values = update_named_values(names, values, message=message, title=title)
    if values:
        values = list(values)
        for name, value in zip(names, values):
            try:
                settings[name] = ast.literal_eval(value)
            except (TypeError, ValueError, SyntaxError):
                settings[name] = value
        return True
    return False


def unload_modules(top_level_module_name):
    """Unloads all modules named starting with the specified string.

    This function eases the development workflow when editing a library that is
    used from Rhino/Grasshopper.

    Parameters
    ----------
    top_level_module_name : :obj:`str`
        Name of the top-level module to unload.

    Returns
    -------
    list
        List of unloaded module names.
    """
    modules = filter(lambda m: m.startswith(top_level_module_name), sys.modules)

    for module in modules:
        sys.modules.pop(module)

    return modules


# src/compas_rhino/utilities/objects.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import json

import compas_rhino

import System
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc

find_object = sc.doc.Objects.Find

try:
    purge_object = sc.doc.Objects.Purge
except AttributeError:
    purge_object = None


__all__ = [
    'find_object',
    'get_objects',
    'get_object_layers',
    'get_object_types',
    'get_object_names',
    'get_object_name',
    'get_object_attributes',
    'set_object_attributes',
    'get_object_attributes_from_name',
    'delete_object',
    'delete_objects',
    'purge_objects',
    'is_curve_line',
    'is_curve_polyline',
    'is_curve_polygon',
    'get_points',
    'get_curves',
    'get_lines',
    'get_polylines',
    'get_polygons',
    'get_point_coordinates',
    'get_line_coordinates',
    'get_polyline_coordinates',
    'get_polygon_coordinates',
    'get_meshes',
    'get_mesh_face_vertices',
    'get_mesh_vertex_coordinates',
    'get_mesh_vertex_colors',
    'set_mesh_vertex_colors',
    'get_mesh_vertices_and_faces',
    'get_mesh_vertex_index',
    'get_mesh_face_index',
    'get_mesh_edge_index',
    'select_object',
    'select_objects',
    'select_point',
    'select_points',
    'select_curve',
    'select_curves',
    'select_line',
    'select_lines',
    'select_polyline',
    'select_polylines',
    'select_polygon',
    'select_polygons',
    'select_surface',
    'select_surfaces',
    'select_mesh',
    'select_meshes',
]


# ==============================================================================
# Objects
# ==============================================================================

def get_objects(name=None, color=None, layer=None, type=None):
    """Get identifiers of Rhino objects, potentially filtered by name, color, layer, or type.

    Parameters
    ----------
    name : str, optional
        Name of the objects.
    color : tuple or list, optional
        RGB color components in integer format (0-255).
    layer : str, optional
        Layer containing the objects.
    type : Rhino.DocObjects.ObjectType, optional
        The object type.

    Returns
    -------
    list[System.Guid]
        The System.Guids of the objects matching the filter parameters.

    Examples
    --------
    .. code-block:: python

        import compas_rhino

        guids_all = compas_rhino.get_objects()
        guids_compas = compas_rhino.get_objects(name='COMPAS.*')
        guids_red = compas_rhino.get_objects(color=(255, 0, 0))
        guids_points = compas_rhino.get_objects(type=compas_rhino.rs.filter.point)
        guids_redpoints = compas_rhino.get_objects(color=(255, 0, 0), type=compas_rhino.rs.filter.point)

    .. code-block:: python

        guids_all = set(compas_rhino.get_objects())
        guids_compas = set(compas_rhino.get_objects(name='COMPAS.*'))
        guids_red = set(compas_rhino.get_objects(color=(255, 0, 0)))
        guids_points = set(compas_rhino.get_objects(type=compas_rhino.rs.filter.point))
        guids_redpoints = set(compas_rhino.get_objects(color=(255, 0, 0), type=compas_rhino.rs.filter.point))

        print guids_compas.issubset(guids_all)
        print guids_all.issubset(guids_compas)

        # True, False

        print guids_red.issubset(guids_all)
        print guids_points.issubset(guids_all)
        print guids_redpoints.issubset(guids_all)

        # True, True, True

        print guids_redpoints.issubset(guids_points)
        print guids_redpoints.issubset(guids_red)
        print guids_points.issubset(guids_red)

        # True, True, False

    """
    guids = rs.AllObjects()
    if name:
        guids = list(set(guids) & set(rs.ObjectsByName(name)))
    if color:
        guids = list(set(guids) & set(rs.ObjectsByColor(color)))
    if layer:
        guids = list(set(guids) & set(rs.ObjectsByLayer(layer)))
    if type:
        guids = list(set(guids) & set(rs.ObjectsByType(type)))
    return guids


def delete_object(guid, purge=None, redraw=True):
    """Delete Rhino object.

    Parameters
    ----------
    guid : System.Guid
        Object identifier.
    purge : None or bool, optional
        If None, the value of the global purge setting (:obj:`compas_rhino.PURGE_ON_DELETE`) will be used.
        If True, purge the object from history after deleting.
        If False, delete but don't purge.
    redraw : bool, optional
        If True, redrawing will be enabled and enacted.
        If False, redrawing will be disabled.

    Returns
    -------
    None

    """
    if purge is None:
        purge = compas_rhino.PURGE_ON_DELETE
    if purge and purge_object:
        purge_objects([guid], redraw=redraw)
    else:
        delete_objects([guid], purge, redraw=redraw)


def delete_objects(guids, purge=None, redraw=True):
    """Delete multiple Rhino objects.

    Parameters
    ----------
    guids : list[System.Guid]
        Object identifiers.
    purge : None or bool, optional
        If None, the value of the global purge setting (:obj:`compas_rhino.PURGE_ON_DELETE`) will be used.
        If True, purge the objects from history after deleting.
        If False, delete but don't purge.
    redraw : bool, optional
        If True, redrawing will be enabled and enacted.
        If False, redrawing will be disabled.

    Returns
    -------
    None

    """
    if purge is None:
        purge = compas_rhino.PURGE_ON_DELETE
    if purge and purge_object:
        purge_objects(guids, redraw=redraw)
    else:
        rs.EnableRedraw(False)
        for guid in guids:
            if rs.IsObjectHidden(guid):
                rs.ShowObject(guid)
        rs.DeleteObjects(guids)
        if redraw:
            rs.EnableRedraw(True)
            sc.doc.Views.Redraw()


def purge_objects(guids, redraw=True):
    """Purge objects from memory.

    Parameters
    ----------
    guids : list[System.Guid]
        Object identifiers.
    redraw : bool, optional
        If True, redrawing will be enabled and enacted.
        If False, redrawing will be disabled.

    Returns
    -------
    None

    """
    if not purge_object:
        raise RuntimeError('Cannot purge outside Rhino script context')
    rs.EnableRedraw(False)
    for guid in guids:
        if rs.IsObject(guid):
            if rs.IsObjectHidden(guid):
                rs.ShowObject(guid)
            o = find_object(guid)
            purge_object(o.RuntimeSerialNumber)
    if redraw:
        rs.EnableRedraw(True)
        sc.doc.Views.Redraw()


def get_object_layers(guids):
    """Get the layer names of multiple objects.

    Parameters
    ----------
    guids : list[System.Guid]
        Object identifiers.

    Returns
    -------
    list[str]

    """
    return [rs.ObjectLayer(guid) for guid in guids]


def get_object_types(guids):
    """Get the type of multiple objects.

    Parameters
    ----------
    guids : list[System.Guid]
        Object identifiers.

    Returns
    -------
    list[str]

    """
    return [rs.ObjectType(guid) for guid in guids]


def get_object_names(guids):
    """Get the names of multiple objects.

    Parameters
    ----------
    guids : list[System.Guid]
        Object identifiers.

    Returns
    -------
    list[str]

    """
    return [rs.ObjectName(guid) for guid in guids]


def get_object_name(guid):
    """Get the name of one object.

    Parameters
    ----------
    guid : System.Guid
        Object identifier.

    Returns
    -------
    str

    """
    return rs.ObjectName(guid)


def get_object_attributes(guid):
    """Get attributes from an object's user dictionary.

    Parameters
    ----------
    guid : System.Guid
        Object identifier.

    Returns
    -------
    dict[str, Any]

    """
    o = find_object(guid)
    u = o.Attributes.UserDictionary
    a = {}
    if u.Count:
        for key in u.Keys:
            a[key] = u.Item[key]
    return a


def set_object_attributes(guid, attr):
    """Set the custom attributes of a Rhino object.

    Parameters
    ----------
    guid : System.Guid
        Object identifier.
    attr : dict[str, Any]
        A dictionary of attributes.

    Returns
    -------
    None

    Raises
    ------
    Exception
        If the operation fails.

    """
    o = find_object(guid)
    u = o.Attributes.UserDictionary
    for name, value in iter(attr.items()):
        try:
            u.Set(name, value)
        except Exception:
            print("The following item cannot be stored in the user dictionary of this object: {0} => {1}".format(name, value))


def get_object_attributes_from_name(guids, prefix=None):
    """Get attributes from JSON parsable object names.

    Parameters
    ----------
    guids : list[System.Guid]
        Object identifiers.
    prefix : str, optional
        A prefix that should be removed before the name is JSON parsable.
        For example, in Rhino 6 and above, names can't start with curly braces ("{").
        Therefore, to make the string representation of a dict a valid name it has to be prefixed with something.
        This prefix can be stripped automatically using this parameter.

    Results
    -------
    list[dict[str, Any]]
        The attribute dicts of the objects.

    """
    load = json.loads
    attrs = []
    for name in get_object_names(guids):
        try:
            if prefix:
                name = name.lstrip(prefix)
            attr = load(name)
        except (ValueError, TypeError):
            attr = {}
        attrs.append(attr)
    return attrs


def select_object(message="Select an object."):
    """Select one object in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected object.

    """
    return rs.GetObject(message)


def select_objects(message='Select multiple objects.'):
    """Select multiple objects in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected objects.

    """
    guids = []
    temp = rs.GetObjects(message)
    if temp:
        return temp
    return guids


# ==============================================================================
# Points
# ==============================================================================


def select_point(message='Select one point.'):
    """Select one point in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected point.

    """
    return rs.GetObject(message, preselect=True, select=True, filter=rs.filter.point)


def select_points(message='Select multiple points.'):
    """Select multiple points in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected points.

    """
    guids = []
    temp = rs.GetObjects(message, preselect=True, select=True, group=False, filter=rs.filter.point)
    if temp:
        guids = temp
    return guids


def get_points(layer=None):
    """Get all points.

    Parameters
    ----------
    layer : str, optional
        Name of a layer containing the points.

    Returns
    -------
    list[System.Guid]
        The identifiers of the points.

    """
    if layer:
        rs.EnableRedraw(False)
        # Argument names for LayerVisible command are not the same for Rhino5 and Rhino6
        # that is why we use positional instead of named arguments
        visible = rs.LayerVisible(layer, True, True)
        guids = rs.ObjectsByType(rs.filter.point)
        guids = list(set(guids) & set(rs.ObjectsByLayer(layer)))
        rs.LayerVisible(layer, visible, True)
        rs.EnableRedraw(True)
    else:
        guids = rs.ObjectsByType(rs.filter.point)
    return guids


def get_point_coordinates(guids):
    """Get the coordintes of the locations of point objects.

    Parameters
    ----------
    guids : list[System.Guid]

    Returns
    -------
    list[[float, float, float]]
        The location coordinates of the points.

    """
    points = []
    for guid in guids:
        point = rs.PointCoordinates(guid)
        if point:
            points.append(map(float, point))
    return points


# ==============================================================================
# Curves
# ==============================================================================


def is_curve_line(guid):
    """Verify that a curve is a line.

    Parameters
    ----------
    guid : System.Guid
        The identifier of the curve.

    Returns
    -------
    bool
        True if the curve is a line.
        False otherwise.

    """
    return rs.IsCurve(guid) and rs.IsLine(guid) and rs.CurveDegree(guid) == 1 and len(rs.CurvePoints(guid)) == 2


def is_curve_polyline(guid):
    return rs.IsCurve(guid) and rs.IsPolyline(guid) and rs.CurveDegree(guid) == 1 and len(rs.CurvePoints(guid)) > 2


def is_curve_polygon(guid):
    return rs.IsCurve(guid) and rs.IsCurveClosed(guid) and rs.CurveDegree(guid) == 1 and len(rs.CurvePoints(guid)) > 2


def select_curve(message='Select one curve.'):
    """Select one curve in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected curve.

    """
    return rs.GetObject(message, preselect=True, select=True, filter=rs.filter.curve)


def select_curves(message='Select multiple curves.'):
    """Select multiple curves in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected curves.

    """
    guids = []
    temp = rs.GetObjects(message, preselect=True, select=True, group=False, filter=rs.filter.curve)
    if temp:
        guids = temp
    return guids


def select_line(message='Select line.'):
    """Select one line in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected line.

    """
    guid = rs.GetObject(message, preselect=True, select=True, filter=rs.filter.curve)
    if is_curve_line(guid):
        return guid
    return None


def select_lines(message='Select multiple lines.'):
    """Select multiple lines in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected lines.

    """
    guids = []
    temp = rs.GetObjects(message, preselect=True, select=True, group=False, filter=rs.filter.curve)
    if temp:
        for guid in temp:
            if is_curve_line(guid):
                guids.append(guid)
    return guids


def select_polyline(message='Select one polyline (curve with degree = 1, and multiple segments).'):
    """Select one polyline in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected polyline.

    """
    guid = rs.GetObject(message, preselect=True, select=True, filter=rs.filter.curve)
    if is_curve_polyline(guid):
        return guid
    return None


def select_polylines(message='Select multiple polylines (curves with degree = 1, and multiple segments).'):
    """Select multiple polylines in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected polylines.

    """
    guids = []
    temp = rs.GetObjects(message, preselect=True, select=True, group=False, filter=rs.filter.curve)
    if temp:
        for guid in temp:
            if is_curve_polyline(guid):
                guids.append(guid)
    return guids


def select_polygon(message='Select one polygon (closed curve with degree = 1)'):
    """Select one polygon in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected polygon.

    """
    guid = rs.GetObject(message, preselect=True, select=True, filter=rs.filter.curve)
    if is_curve_polygon(guid):
        return guid
    return None


def select_polygons(message='Select multiple polygons (closed curves with degree = 1)'):
    """Select multiple polygons in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected polygons.

    """
    guids = []
    temp = rs.GetObjects(message, preselect=True, select=True, group=False, filter=rs.filter.curve)
    if temp:
        for guid in temp:
            if is_curve_polygon(guid):
                guids.append(guid)
    return guids


def get_curves(layer=None):
    """Get all curves.

    Parameters
    ----------
    layer : str, optional
        Name of a layer containing the curves.

    Returns
    -------
    list[System.Guid]
        The identifiers of the curves.

    """
    if layer:
        rs.EnableRedraw(False)
        # Argument names for LayerVisible command are not the same for Rhino5 and Rhino6
        # that is why we use positional instead of named arguments
        visible = rs.LayerVisible(layer, True, True)
        guids = rs.ObjectsByType(rs.filter.curve)
        guids = list(set(guids) & set(rs.ObjectsByLayer(layer)))
        rs.LayerVisible(layer, visible, True)
        rs.EnableRedraw(True)
    else:
        guids = rs.ObjectsByType(rs.filter.curve)
    return guids


def get_lines(layer=None):
    """Get all lines.

    Parameters
    ----------
    layer : str, optional
        Name of a layer containing the lines.

    Returns
    -------
    list[System.Guid]
        The identifiers of the lines.

    """
    if layer:
        rs.EnableRedraw(False)
        # Argument names for LayerVisible command are not the same for Rhino5 and Rhino6
        # that is why we use positional instead of named arguments
        visible = rs.LayerVisible(layer, True, True)
        guids = rs.ObjectsByType(rs.filter.curve)
        guids = [guid for guid in guids if is_curve_line(guid)]
        guids = list(set(guids) & set(rs.ObjectsByLayer(layer)))
        rs.LayerVisible(layer, visible, True)
        rs.EnableRedraw(True)
    else:
        guids = rs.ObjectsByType(rs.filter.curve)
        guids = [guid for guid in guids if is_curve_line(guid)]
    return guids


def get_polylines(layer=None):
    """Get all polylines.

    Parameters
    ----------
    layer : str, optional
        Name of a layer containing the polylines.

    Returns
    -------
    list[System.Guid]
        The identifiers of the polylines.

    """
    if layer:
        rs.EnableRedraw(False)
        # Argument names for LayerVisible command are not the same for Rhino5 and Rhino6
        # that is why we use positional instead of named arguments
        visible = rs.LayerVisible(layer, True, True)
        guids = rs.ObjectsByType(rs.filter.curve)
        guids = [guid for guid in guids if is_curve_polyline(guid)]
        guids = list(set(guids) & set(rs.ObjectsByLayer(layer)))
        rs.LayerVisible(layer, visible, True)
        rs.EnableRedraw(True)
    else:
        guids = rs.ObjectsByType(rs.filter.curve)
        guids = [guid for guid in guids if is_curve_polyline(guid)]
    return guids


def get_polygons(layer=None):
    """Get all polygons.

    Parameters
    ----------
    layer : str, optional
        Name of a layer containing the polygons.

    Returns
    -------
    list[System.Guid]
        The identifiers of the polygons.

    """
    if layer:
        rs.EnableRedraw(False)
        # Argument names for LayerVisible command are not the same for Rhino5 and Rhino6
        # that is why we use positional instead of named arguments
        visible = rs.LayerVisible(layer, True, True)
        guids = rs.ObjectsByType(rs.filter.curve)
        guids = [guid for guid in guids if is_curve_polygon(guid)]
        guids = list(set(guids) & set(rs.ObjectsByLayer(layer)))
        rs.LayerVisible(layer, visible, True)
        rs.EnableRedraw(True)
    else:
        guids = rs.ObjectsByType(rs.filter.curve)
        guids = [guid for guid in guids if is_curve_polygon(guid)]
    return guids


def get_line_coordinates(guids):
    """Get the start and end point coordinates of line curves.

    Parameters
    ----------
    guids : list[System.Guid]
        Line curve identifiers.

    Returns
    -------
    list[tuple[[float, float, float], [float, float, float]]]
        A start and end point per line.

    """
    if isinstance(guids, System.Guid):
        sp = map(float, rs.CurveStartPoint(guids))
        ep = map(float, rs.CurveEndPoint(guids))
        return sp, ep
    lines = []
    for guid in guids:
        sp = map(float, rs.CurveStartPoint(guid))
        ep = map(float, rs.CurveEndPoint(guid))
        lines.append((sp, ep))
    return lines


def get_polyline_coordinates(guids):
    """Get the point coordinates of polylines.

    Parameters
    ----------
    guids : list[System.Guid]
        Polyline curve identifiers.

    Returns
    -------
    list[list[[float, float, float]]]
        A list of point coordinates per polyline.

    """
    if isinstance(guids, System.Guid):
        points = rs.PolylineVertices(guids)
        coords = []
        if points:
            coords = [map(float, point) for point in points]
        return coords
    polylines = []
    for guid in guids:
        points = rs.PolylineVertices(guid)
        coords = []
        if points:
            coords = [map(float, point) for point in points]
        polylines.append(coords)
    return polylines


def get_polygon_coordinates(guids):
    """Get the point coordinates of polygons.

    Parameters
    ----------
    guids : list[System.Guid]
        Polygon curve identifiers.

    Returns
    -------
    list[list[[float, float, float]]]
        A list of point coordinates per polygon.

    """
    if isinstance(guids, System.Guid):
        points = rs.CurvePoints(guids)
        coords = []
        if points:
            coords = [list(point) for point in points]
        return coords
    polygons = []
    if guids:
        for guid in guids:
            points = rs.CurvePoints(guid)
            coords = []
            if points:
                coords = map(list, points)
            polygons.append(coords)
    return polygons


# ==============================================================================
# Surfaces
# ==============================================================================


def select_surface(message='Select one surface.'):
    """Select one surface in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected surface.

    """
    return rs.GetObject(
        message, preselect=True, select=True,
        filter=rs.filter.surface | rs.filter.polysurface)


def select_surfaces(message='Select multiple surfaces.'):
    """Select multiple surfaces in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected surfaces.

    """
    guids = []
    temp = rs.GetObjects(
        message, preselect=True, select=True, group=False,
        filter=rs.filter.surface | rs.filter.polysurface)
    if temp:
        guids = temp
    return guids


# ==============================================================================
# Meshes
# ==============================================================================


def select_mesh(message='Select one mesh.'):
    """Select one mesh in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    System.Guid
        The identifer of the selected mesh.

    """
    return rs.GetObject(
        message, preselect=True, select=True,
        filter=rs.filter.mesh
    )


def select_meshes(message='Select multiple meshes.'):
    """Select multiple meshes in the Rhino view.

    Parameters
    ----------
    message : str, optional
        Instruction for the user.

    Returns
    -------
    list[System.Guid]
        The identifers of the selected meshs.

    """
    guids = []
    temp = rs.GetObjects(message, preselect=True, select=True, group=False, filter=rs.filter.mesh)
    if temp:
        guids = temp
    return guids


def get_meshes(layer=None):
    if layer:
        rs.EnableRedraw(False)
        # Argument names for LayerVisible command are not the same for Rhino5 and Rhino6
        # that is why we use positional instead of named arguments
        visible = rs.LayerVisible(layer, True, True)
        guids = rs.ObjectsByType(rs.filter.mesh)
        guids = list(set(guids) & set(rs.ObjectsByLayer(layer)))
        rs.LayerVisible(layer, visible, True)
        rs.EnableRedraw(True)
    else:
        guids = rs.ObjectsByType(rs.filter.mesh)
    return guids


def get_mesh_border(guid):
    return rs.DuplicateMeshBorder(guid)


def get_mesh_face_vertices(guid):
    faces = []
    if guid:
        temp = rs.MeshFaceVertices(guid)
        faces = map(list, temp)
    return faces


def get_mesh_vertex_coordinates(guid):
    vertices = []
    if guid:
        vertices = [map(float, vertex) for vertex in rs.MeshVertices(guid)]
    return vertices


def get_mesh_vertex_colors(guid):
    colors = []
    if guid:
        temp = rs.MeshVertexColors(guid)
        if temp:
            colors = map(list, temp)
    return colors


def set_mesh_vertex_colors(guid, colors):
    if not guid:
        return
    return rs.MeshVertexColors(guid, colors)


def get_mesh_vertices_and_faces(guid):
    if not guid:
        return
    vertices = [map(float, vertex) for vertex in rs.MeshVertices(guid)]
    faces = map(list, rs.MeshFaceVertices(guid))
    return vertices, faces


def get_mesh_vertex_index(guid):
    class CustomGetObject(Rhino.Input.Custom.GetObject):
        def CustomGeometryFilter(self, rhino_object, geometry, component_index):
            return guid == rhino_object.Id
    go = CustomGetObject()
    go.SetCommandPrompt('Select one vertex of the mesh.')
    go.GeometryFilter = Rhino.DocObjects.ObjectType.MeshVertex
    go.AcceptNothing(True)
    if go.Get() != Rhino.Input.GetResult.Object:
        return None
    objref = go.Object(0)
    if not objref:
        return None
    tvindex = objref.GeometryComponentIndex.Index
    mobj = sc.doc.Objects.Find(guid)
    mgeo = mobj.Geometry
    temp = mgeo.TopologyVertices.MeshVertexIndices(tvindex)
    vindex = temp[0]
    go.Dispose()
    return vindex


def get_mesh_face_index(guid):
    class CustomGetObject(Rhino.Input.Custom.GetObject):
        def CustomGeometryFilter(self, rhino_object, geometry, component_index):
            return guid == rhino_object.Id
    go = CustomGetObject()
    go.SetCommandPrompt('Select one face of the mesh.')
    go.GeometryFilter = Rhino.DocObjects.ObjectType.MeshFace
    go.AcceptNothing(True)
    if go.Get() != Rhino.Input.GetResult.Object:
        return None
    objref = go.Object(0)
    if not objref:
        return None
    findex = objref.GeometryComponentIndex.Index
    go.Dispose()
    return findex


def get_mesh_edge_index(guid):
    class CustomGetObject(Rhino.Input.Custom.GetObject):
        def CustomGeometryFilter(self, rhino_object, geometry, component_index):
            return guid == rhino_object.Id
    go = CustomGetObject()
    go.SetCommandPrompt('Select an edge of the mesh.')
    go.GeometryFilter = Rhino.DocObjects.ObjectType.MeshEdge
    go.AcceptNothing(True)
    if go.Get() != Rhino.Input.GetResult.Object:
        return None
    objref = go.Object(0)
    if not objref:
        return None
    eindex = objref.GeometryComponentIndex.Index
    go.Dispose()
    return eindex


def get_mesh_vertex_indices(guid):
    tvindices = rs.GetMeshVertices(guid, 'Select mesh vertices.')
    if not tvindices:
        return
    mobj = sc.doc.Objects.Find(guid)
    mgeo = mobj.Geometry
    vindices = []
    for tvindex in tvindices:
        temp = mgeo.TopologyVertices.MeshVertexIndices(tvindex)
        vindices.append(temp[0])
    return vindices


def get_mesh_face_indices(guid):
    return rs.GetMeshFaces(guid, 'Select mesh faces.')


def get_mesh_vertex_face_indices(guid):
    vindex = get_mesh_vertex_index(guid)
    if vindex is None:
        return
    mobj = sc.doc.Objects.Find(guid)
    mgeo = mobj.Geometry
    findices = mgeo.TopologyVertices.ConnectedFaces(vindex)
    return findices


def get_mesh_face_vertex_indices(guid):
    findex = get_mesh_face_index(guid)
    if findex is None:
        return
    mobj = sc.doc.Objects.Find(guid)
    mgeo = mobj.Geometry
    tvertices = mgeo.Faces.GetTopologicalVertices(findex)
    vindices = []
    for tvertex in tvertices:
        temp = mgeo.TopologyVertices.MeshVertexIndices(tvertex)
        vindices.append(temp[0])
    return vindices


def get_mesh_edge_vertex_indices(guid):
    eindex = get_mesh_edge_index(guid)
    if eindex is None:
        return
    mobj = sc.doc.Objects.Find(guid)
    mgeo = mobj.Geometry
    temp = mgeo.TopologyEdges.GetTopologyVertices(eindex)
    tvindices = temp.I, temp.J
    vindices = []
    for tvindex in tvindices:
        temp = mgeo.TopologyVertices.MeshVertexIndices(tvindex)
        vindices.append(temp[0])
    return vindices


# src/compas_rhino/geometry/booleans/__init__.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import Rhino

from compas.plugins import plugin


__all__ = [
    'boolean_union_mesh_mesh',
    'boolean_difference_mesh_mesh',
    'boolean_intersection_mesh_mesh',
]


@plugin(category='booleans', requires=['Rhino'])
def boolean_union_mesh_mesh(A, B, remesh=False):
    """Compute the boolean union of two triangle meshes.

    Parameters
    ----------
    A : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        The vertices and faces of mesh A.
    B : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        The vertices and faces of mesh B.
    remesh : bool, optional
        If True, remesh the result.

    Returns
    -------
    sequence[[float, float, float]]
        The vertices of the boolean union.
    sequence[[int, int, int]]
        The faces of the boolean union.

    """
    return _boolean_operation(A, B, lambda a, b: Rhino.Geometry.Mesh.CreateBooleanUnion([a, b]))


@plugin(category='booleans', requires=['Rhino'])
def boolean_difference_mesh_mesh(A, B, remesh=False):
    """Compute the boolean difference of two triangle meshes.

    Parameters
    ----------
    A : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        The vertices and faces of mesh A.
    B : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        The vertices and faces of mesh B.
    remesh : bool, optional
        If True, remesh the result.

    Returns
    -------
    sequence[[float, float, float]]
        The vertices of the boolean difference.
    sequence[[int, int, int]]
        The faces of the boolean difference.

    """
    return _boolean_operation(A, B, lambda a, b: Rhino.Geometry.Mesh.CreateBooleanDifference([a], [b]))


@plugin(category='booleans', requires=['Rhino'])
def boolean_intersection_mesh_mesh(A, B, remesh=False):
    """Compute the boolean intersection of two triangle meshes.

    Parameters
    ----------
    A : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        The vertices and faces of mesh A.
    B : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        The vertices and faces of mesh B.
    remesh : bool, optional
        If True, remesh the result.

    Returns
    -------
    sequence[[float, float, float]]
        The vertices of the boolean intersection.
    sequence[[int, int, int]]
        The faces of the boolean intersection.

    """
    return _boolean_operation(A, B, lambda a, b: Rhino.Geometry.Mesh.CreateBooleanIntersection([a], [b]))


def _boolean_operation(A, B, method):
    meshes = []
    for vertices, faces in [A, B]:
        mesh = Rhino.Geometry.Mesh()
        for x, y, z in vertices:
            mesh.Vertices.Add(x, y, z)
        for face in faces:
            mesh.Faces.AddFace(*face)
        meshes.append(mesh)
    mesh_a, mesh_b = meshes

    result = method(mesh_a, mesh_b)

    # Rhino SDK returns None on failure
    if not result:
        return None

    vertices = []
    faces = []

    for mesh in result:
        for face in mesh.Faces:
            reindexed_face = [v + len(vertices) for v in (face.A, face.B, face.C)]
            faces.append(reindexed_face)

        vertices += ((v.X, v.Y, v.Z) for v in mesh.Vertices)

    return vertices, faces


# src/compas_rhino/geometry/curves/__init__.py

from .nurbs import RhinoNurbsCurve

from compas.geometry import NurbsCurve
from compas.plugins import plugin


@plugin(category='factories', requires=['Rhino'])
def new_nurbscurve(*args, **kwargs):
    return super(NurbsCurve, RhinoNurbsCurve).__new__(RhinoNurbsCurve)


@plugin(category='factories', requires=['Rhino'])
def new_nurbscurve_from_parameters(*args, **kwargs):
    return RhinoNurbsCurve.from_parameters(*args, **kwargs)


@plugin(category='factories', requires=['Rhino'])
def new_nurbscurve_from_points(*args, **kwargs):
    return RhinoNurbsCurve.from_points(*args, **kwargs)


@plugin(category='factories', requires=['Rhino'])
def new_nurbscurve_from_interpolation(*args, **kwargs):
    return RhinoNurbsCurve.from_interpolation(*args, **kwargs)


@plugin(category='factories', requires=['Rhino'])
def new_nurbscurve_from_step(*args, **kwargs):
    return RhinoNurbsCurve.from_step(*args, **kwargs)


# src/compas_rhino/geometry/curves/nurbs.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from itertools import groupby
from compas.geometry import Point
from compas.utilities import linspace

from compas.geometry import NurbsCurve

from compas_rhino.conversions import point_to_rhino
from compas_rhino.conversions import point_to_compas
from compas_rhino.conversions import vector_to_compas
from compas_rhino.conversions import circle_to_rhino
from compas_rhino.conversions import ellipse_to_rhino
from compas_rhino.conversions import line_to_rhino
from compas_rhino.conversions import xform_to_rhino
from compas_rhino.conversions import plane_to_compas_frame
from compas_rhino.conversions import box_to_compas

import Rhino.Geometry


def rhino_curve_from_parameters(points, weights, knots, multiplicities, degree):
    rhino_curve = Rhino.Geometry.NurbsCurve(3, True, degree + 1, len(points))
    for index, (point, weight) in enumerate(zip(points, weights)):
        rhino_curve.Points.SetPoint(index, point_to_rhino(point), weight)
    knotvector = [knot for knot, mult in zip(knots, multiplicities) for _ in range(mult)]
    # account for superfluous knots
    # https://developer.rhino3d.com/guides/opennurbs/superfluous-knots/
    p = len(points)
    o = degree + 1
    k = p + o
    if len(knotvector) == k:
        knotvector[:] = knotvector[1:-1]
    for index, knot in enumerate(knotvector):
        rhino_curve.Knots[index] = knot
    return rhino_curve


class RhinoNurbsCurve(NurbsCurve):
    """Class representing a NURBS curve based on the NurbsCurve of Rhino.Geometry.

    Parameters
    ----------
    name : str, optional
        Name of the curve.

    Attributes
    ----------
    points : list[:class:`compas.geometry.Point`], read-only
        The control points of the curve.
    weights : list[float], read-only
        The weights of the control points.
    knots : list[float], read-only
        The knot vector, without duplicates.
    multiplicities : list[int], read-only
        The multiplicities of the knots in the knot vector.
    knotsequence : list[float], read-only
        The knot vector, with repeating values according to the multiplicities.
    degree : int, read-only
        The degree of the polynomials.
    order : int, read-only
        The order of the curve.
    domain : tuple[float, float], read-only
        The parameter domain.
    start : :class:`compas.geometry.Point`, read-only
        The point corresponding to the start of the parameter domain.
    end : :class:`compas.geometry.Point`, read-only
        The point corresponding to the end of the parameter domain.
    is_closed : bool, read-only
        True if the curve is closed.
    is_periodic : bool, read-only
        True if the curve is periodic.
    is_rational : bool, read-only
        True is the curve is rational.

    References
    ----------
    .. [2] https://developer.rhino3d.com/api/RhinoCommon/html/T_Rhino_Geometry_NurbsCurve.htm
    .. [3] https://en.wikipedia.org/wiki/Non-uniform_rational_B-spline
    .. [4] https://developer.rhino3d.com/guides/opennurbs/nurbs-geometry-overview/

    """

    def __init__(self, name=None):
        super(RhinoNurbsCurve, self).__init__(name=name)
        self.rhino_curve = None

    def __eq__(self, other):
        return self.rhino_curve.IsEqual(other.rhino_curve)

    # ==============================================================================
    # Data
    # ==============================================================================

    @property
    def data(self):
        # add superfluous knots
        # for compatibility with all/most other NURBS implementations
        # https://developer.rhino3d.com/guides/opennurbs/superfluous-knots/
        multiplicities = self.multiplicities[:]
        multiplicities[0] += 1
        multiplicities[-1] += 1
        return {
            'points': [point.data for point in self.points],
            'weights': self.weights,
            'knots': self.knots,
            'multiplicities': multiplicities,
            'degree': self.degree,
            'is_periodic': self.is_periodic,
        }

    @data.setter
    def data(self, data):
        points = [Point.from_data(point) for point in data['points']]
        weights = data['weights']
        knots = data['knots']
        multiplicities = data['multiplicities']
        degree = data['degree']
        # is_periodic = data['is_periodic']
        # have not found a way to actually set this
        # not sure if that is actually possible...
        self.rhino_curve = rhino_curve_from_parameters(points, weights, knots, multiplicities, degree)

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_rhino(cls, rhino_curve):
        """Construct a NURBS curve from an existing Rhino curve.

        Parameters
        ----------
        rhino_curve : Rhino.Geometry.NurbsCurve

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        curve = cls()
        curve.rhino_curve = rhino_curve
        return curve

    @classmethod
    def from_parameters(cls, points, weights, knots, multiplicities, degree, is_periodic=False):
        """Construct a NURBS curve from explicit curve parameters.

        Parameters
        ----------
        points : list[:class:`compas.geometry.Point`]
            The control points.
        weights : list[float]
            The control point weights.
        knots : list[float]
            The curve knots, without duplicates.
        multiplicities : list[int]
            The multiplicities of the knots.
        degree : int
            The degree of the curve.
        is_periodic : bool, optional
            Flag indicating whether the curve is periodic or not.
            Note that this parameters is currently not supported.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        curve = cls()
        curve.rhino_curve = rhino_curve_from_parameters(points, weights, knots, multiplicities, degree)
        return curve

    @classmethod
    def from_points(cls, points, degree=3, is_periodic=False):
        """Construct a NURBS curve from control points.

        Parameters
        ----------
        points : list[:class:`compas.geometry.Point`]
            The control points.
        degree : int, optional
            The degree of the curve.
        is_periodic : bool, optional
            Flag indicating whether the curve is periodic or not.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        points[:] = [point_to_rhino(point) for point in points]
        curve = cls()
        curve.rhino_curve = Rhino.Geometry.NurbsCurve.Create(is_periodic, degree, points)
        return curve

    @classmethod
    def from_interpolation(cls, points, precision=1e-3):
        """Construct a NURBS curve by interpolating a set of points.

        Parameters
        ----------
        points : list[:class:`compas.geometry.Point`]
            The control points.
        precision : float, optional
            The required precision of the interpolation.
            This parameter is currently not supported.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        curve = cls()
        curve.rhino_curve = Rhino.Geometry.NurbsCurve.CreateHSpline([point_to_rhino(point) for point in points])
        return curve

    @classmethod
    def from_step(cls, filepath):
        """Load a NURBS curve from an STP file.

        Parameters
        ----------
        filepath : str
            A filepath.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        raise NotImplementedError

    @classmethod
    def from_arc(cls, arc):
        """Construct a NURBS curve from an arc.

        Parameters
        ----------
        arc : :class:`compas.geometry.Arc`
            An arc geometry.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        # curve = cls()
        # curve.rhino_curve = Rhino.Geometry.NurbsCurve.CreateFromArc(arc_to_rhino(arc))
        # return curve
        raise NotImplementedError

    @classmethod
    def from_circle(cls, circle):
        """Construct a NURBS curve from a circle.

        Parameters
        ----------
        circle : :class:`compas.geometry.Circle`
            A circle geometry.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        curve = cls()
        curve.rhino_curve = Rhino.Geometry.NurbsCurve.CreateFromCircle(circle_to_rhino(circle))
        return curve

    @classmethod
    def from_ellipse(cls, ellipse):
        """Construct a NURBS curve from an ellipse.

        Parameters
        ----------
        ellipse : :class:`compas.geometry.Ellipse`
            An ellipse geometry.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        curve = cls()
        curve.rhino_curve = Rhino.Geometry.NurbsCurve.CreateFromEllipse(ellipse_to_rhino(ellipse))
        return curve

    @classmethod
    def from_line(cls, line):
        """Construct a NURBS curve from a line.

        Parameters
        ----------
        line : :class:`compas.geometry.Line`
            A line geometry.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        curve = cls()
        curve.rhino_curve = Rhino.Geometry.NurbsCurve.CreateFromLine(line_to_rhino(line))
        return curve

    # ==============================================================================
    # Conversions
    # ==============================================================================

    def to_step(self, filepath, schema="AP203"):
        """Write the curve geometry to a STP file."""
        raise NotImplementedError

    def to_line(self):
        """Convert the NURBS curve to a line."""
        raise NotImplementedError

    def to_polyline(self):
        """Convert the NURBS curve to a polyline."""
        raise NotImplementedError

    # ==============================================================================
    # Rhino
    # ==============================================================================

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def points(self):
        if self.rhino_curve:
            return [point_to_compas(point) for point in self.rhino_curve.Points]

    @property
    def weights(self):
        if self.rhino_curve:
            return [point.Weight for point in self.rhino_curve.Points]

    @property
    def knots(self):
        if self.rhino_curve:
            return [key for key, _ in groupby(self.rhino_curve.Knots)]

    @property
    def knotsequence(self):
        if self.rhino_curve:
            return list(self.rhino_curve.Knots)

    @property
    def multiplicities(self):
        if self.rhino_curve:
            return [len(list(group)) for _, group in groupby(self.rhino_curve.Knots)]

    @property
    def degree(self):
        if self.rhino_curve:
            return self.rhino_curve.Degree

    @property
    def dimension(self):
        if self.rhino_curve:
            return self.rhino_curve.Dimension

    @property
    def domain(self):
        if self.rhino_curve:
            return self.rhino_curve.Domain.T0, self.rhino_curve.Domain.T1

    @property
    def order(self):
        if self.rhino_curve:
            return self.rhino_curve.Order

    @property
    def start(self):
        if self.rhino_curve:
            return point_to_compas(self.rhino_curve.PointAtStart)

    @property
    def end(self):
        if self.rhino_curve:
            return point_to_compas(self.rhino_curve.PointAtEnd)

    @property
    def is_closed(self):
        if self.rhino_curve:
            return self.rhino_curve.IsClosed

    @property
    def is_periodic(self):
        if self.rhino_curve:
            return self.rhino_curve.IsPeriodic

    @property
    def is_rational(self):
        if self.rhino_curve:
            return self.rhino_curve.IsRational

    # ==============================================================================
    # Methods
    # ==============================================================================

    def copy(self):
        """Make an independent copy of the current curve.

        Returns
        -------
        None

        """
        cls = type(self)
        curve = cls()
        curve.rhino_curve = self.rhino_curve.Duplicate()
        return curve

    def transform(self, T):
        """Transform this curve.

        Parameters
        ----------
        T : :class:`compas.geometry.Transformation`
            A COMPAS transformation.

        Returns
        -------
        None

        """
        self.rhino_curve.Transform(xform_to_rhino(T))

    def transformed(self, T):
        """Transform a copy of the curve.

        Parameters
        ----------
        T : :class:`compas.geometry.Transformation`
            A COMPAS transformation.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsCurve`

        """
        copy = self.copy()
        copy.transform(T)
        return copy

    def reverse(self):
        """Reverse the parametrisation of the curve.

        Returns
        -------
        None

        """
        self.rhino_curve.Reverse()

    def space(self, n=10):
        """Compute evenly spaced parameters over the curve domain.

        Parameters
        ----------

        Returns
        -------
        generator[float]

        """
        u, v = self.domain
        return linspace(u, v, n)

    def xyz(self, n=10):
        """Compute point locations corresponding to evenly spaced parameters over the curve domain."""
        return [self.point_at(param) for param in self.space(n)]

    def locus(self, resolution=100):
        """Compute the locus of the curve.

        Parameters
        ----------
        resolution : int
            The number of intervals at which a point on the
            curve should be computed. Defaults to 100.

        Returns
        -------
        list
            Points along the curve.
        """
        return self.xyz(resolution)

    def point_at(self, t):
        """Compute a point on the curve.

        Parameters
        ----------
        t : float
            The value of the curve parameter. Must be between 0 and 1.

        Returns
        -------
        :class:`compas.geometry.Point`
            the corresponding point on the curve.
        """
        point = self.rhino_curve.PointAt(t)
        return point_to_compas(point)

    def tangent_at(self, t):
        """Compute the tangent vector at a point on the curve.

        Parameters
        ----------
        t : float
            The value of the curve parameter. Must be between 0 and 1.

        Returns
        -------
        :class:`compas.geometry.Vector`
            The corresponding tangent vector.

        """
        vector = self.rhino_curve.TangentAt(t)
        return vector_to_compas(vector)

    def curvature_at(self, t):
        """Compute the curvature at a point on the curve.

        Parameters
        ----------
        t : float
            The value of the curve parameter. Must be between 0 and 1.

        Returns
        -------
        :class:`compas.geometry.Vector`
            The corresponding curvature vector.

        """
        vector = self.rhino_curve.CurvatureAt(t)
        return vector_to_compas(vector)

    def frame_at(self, t):
        """Compute the local frame at a point on the curve.

        Parameters
        ----------
        t : float
            The value of the curve parameter. Must be between 0 and 1.

        Returns
        -------
        :class:`compas.geometry.Frame`
            The corresponding local frame.

        """
        plane = self.rhino_curve.FrameAt(t)
        return plane_to_compas_frame(plane)

    def closest_point(self, point, return_parameter=False):
        """Compute the closest point on the curve to a given point.

        Parameters
        ----------
        point : :class:`compas.geometry.Point`
            The point to project orthogonally to the curve.
        return_parameter : bool, optional
            Return the curve parameter in addition to the projected point.

        Returns
        -------
        :class:`compas.geometry.Point` or tuple of :class:`compas.geometry.Point` and float
            The nearest point on the curve, if ``parameter`` is false.
            The nearest as (point, parameter) tuple, if ``parameter`` is true.
        """
        result, t = self.rhino_curve.ClosestPoint(point_to_rhino(point))
        if not result:
            return
        point = self.point_at(t)
        if return_parameter:
            return point, t
        return point

    def divide_by_count(self, count, return_points=False):
        """Divide the curve into a specific number of equal length segments.

        Parameters
        ----------
        count : int
            The number of segments.
        return_points : bool, optional
            If True, return the list of division parameters,
            and the points corresponding to those parameters.
            If False, return only the list of parameters.

        Returns
        -------
        list[float] or tuple of list[float] and list[:class:`compas.geometry.Point`]
            The parameters defining the discretisation,
            and potentially the points corresponding to those parameters.
        """
        params = self.rhino_curve.DivideByCount(count, True)
        if return_points:
            points = [self.point_at(t) for t in params]
            return params, points
        return params

    def divide_by_length(self, length, return_points=False):
        """Divide the curve into segments of specified length.

        Parameters
        ----------
        length : float
            The length of the segments.
        return_points : bool, optional
            If True, return the list of division parameters,
            and the points corresponding to those parameters.
            If False, return only the list of parameters.

        Returns
        -------
        list[float] or tuple of list[float] and list[:class:`compas.geometry.Point`]
            The parameters defining the discretisation,
            and potentially the points corresponding to those parameters.
        """
        params = self.rhino_curve.DivideByLength(length, True)
        if return_points:
            points = [self.point_at(t) for t in params]
            return params, points
        return params

    def aabb(self):
        """Compute the axis aligned bounding box of the curve.

        Returns
        -------
        :class:`compas.geometry.Box`
        """
        box = self.rhino_curve.getBoundingBox(True)
        return box_to_compas(box)

    def obb(self, precision=0.0):
        """Compute the oriented bounding box of the curve."""
        raise NotImplementedError

    def length(self, precision=1e-8):
        """Compute the length of the curve."""
        return self.rhino_curve.GetLength(precision)

    def segment(self, u, v, precision=1e-3):
        """Modifies this curve by segmenting it between the parameters u and v.

        Parameters
        ----------
        u: float
        v: float
        tol: float, optional
            default value is 1e-3

        Returns
        -------
        None

        """
        if u > v:
            u, v = v, u
        s, e = self.domain
        if u < s or v > e:
            raise ValueError('At least one of the given parameters is outside the curve domain.')
        if u == v:
            raise ValueError('The given domain is zero length.')
        raise NotImplementedError

    def segmented(self, u, v, precision=1e-3):
        """Returns a copy of this curve by segmenting it between the parameters u and v.

        Parameters
        ----------
        u: float
        v: float
        tol: float,optional
            default value is 1e-3

        Returns
        -------
        NurbsCurve

        """
        copy = self.copy()
        copy.segment(u, v, precision)
        return copy


# src/compas_rhino/geometry/transformations/__init__.py

from __future__ import absolute_import

import warnings

warnings.warn(
    "Conversion functions for transformations have been moved to `compas_rhino.conversions`.",
    DeprecationWarning)

from compas_rhino.conversions._transformations import *  # noqa : F401 F403


# src/compas_rhino/geometry/trimesh/__init__.py

from __future__ import absolute_import

from .curvature import *  # noqa : F401 F403
from .slice import *  # noqa : F401 F403

__all__ = [name for name in dir() if not name.startswith('_')]


# src/compas_rhino/geometry/trimesh/curvature.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import Rhino
from Rhino.Geometry.Vector3d import Multiply, CrossProduct
from Rhino.Geometry.Point3d import FromPoint3f

from math import pi
from math import sqrt
from math import atan2

from clr import StrongBox
from scriptcontext import doc
from System import Array

from compas.plugins import plugin


__all__ = [
    'trimesh_gaussian_curvature',
    'trimesh_mean_curvature',
    'trimesh_principal_curvature',
]


@plugin(category="trimesh", requires=['Rhino'])
def trimesh_gaussian_curvature(M):

    r"""Compute the discrete Gaussian curvature of a triangle mesh.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.

    Returns
    -------
    list[float]
        The discrete Gaussian curvature per vertex.

    Notes
    -----
    Description: The angle defect at a vertex is used to describe the Gaussian curvature in a neighborhood around a vertex.

    Notation Convention:
        * :math:`K_{i}` - discrete Gaussian curvature at vertex i
        * :math:`j,k` - the vertices from the Star of vertex i
        * :math:`e_{ij},\, e_{ik}` - the vectors from vertex i to j and i to k
        * :math:`\\theta_{i}^{jk}` - interior angle at vertex i of triangle ijk

    Formula:

    .. math::

        K_{i} = 2\pi-\sum\\theta_{i}^{jk}

    References
    ----------
    .. [1] Formula of Discrete Gaussian Curvature available at Keenan Crane's lecture, 03:16-07:11, at https://youtu.be/sokeN5VxBB8

    Examples
    --------
    Make a mesh from scratch
    >>> from compas.geometry import Sphere
    >>> sphere = Sphere([1, 1, 1], 1)
    >>> sphere = Mesh.from_shape(sphere, u=30, v=30)
    >>> sphere.quads_to_triangles()
    >>> M = sphere.to_vertices_and_faces()

    Compute the discrete Gaussian curvature
    >>> K = trimesh_gaussian_curvature(M)

    """
    # (0) see if input is already Rhino.Geometry.Mesh
    mesh = Rhino.Geometry.Mesh()
    if not isinstance(M, Rhino.Geometry.Mesh):
        for x, y, z in M[0]:
            mesh.Vertices.Add(x, y, z)
        for face in M[1]:
            mesh.Faces.AddFace(*face)
    else:
        mesh = M

    # (1) check if it is a trimesh
    if mesh.Faces.QuadCount > 0:
        raise ValueError("Mesh is not trimesh.")

    # (2) Prepare ingredient and return list
    pi_2 = 2 * pi
    K = []

    # (3) Main - loop every vertex for angle defect
    for i in range(mesh.Vertices.Count):
        vert_neighbors_topo = mesh.TopologyVertices.ConnectedTopologyVertices(mesh.TopologyVertices.TopologyVertexIndex(i), True)
        vert_neighbors = []
        if vert_neighbors_topo is None:
            K.append(None)
            continue
        for vert in vert_neighbors_topo:
            vert_neighbors.extend(mesh.TopologyVertices.MeshVertexIndices(vert))
        angles = []
        valence = len(vert_neighbors)
        v_i = mesh.Vertices[i]
        # loop every neighbor
        for j in range(valence):
            v_j = mesh.Vertices[vert_neighbors[j]]
            v_k = mesh.Vertices[vert_neighbors[(j + 1) % valence]]
            e_ij = v_j - v_i
            e_ik = v_k - v_i
            angles.append(Rhino.Geometry.Vector3d.VectorAngle(e_ij, e_ik))
        K.append(pi_2 - sum(angles))
    # (4) Output
    return K


@plugin(category="trimesh", requires=['Rhino'])
def trimesh_mean_curvature(M):
    r"""Compute the discrete mean curvature of a triangle mesh.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.

    Returns
    -------
    list[float]
        The discrete mean curvature per vertex.

    Notes
    -----
    Description: The discrete mean curvature is computed by edge length and its dihedral angle.

    Notation Convention:
        * :math:`H_{i}` - discrete mean curvature at vertex i
        * :math:`E` - all the edges connected to vertex i
        * :math:`j` - a vertex connected to vertex i
        * :math:`l_{ij}` - the length of edge ij
        * :math:`\phi_{ij}` - the dihedral angle of edge ij

    Formula:

    .. math::

        H_{i} = \frac{1}{4}\sum_{ij\in E}l_{ij}\phi_{ij}

    References
    ----------
    .. [1] Formula of Discrete Mean Curvature available at Keenan Crane's lecture, 03:16-07:11, at https://youtu.be/sokeN5VxBB8
    .. [2] Formula of dihedral angle available at Keenan Crane's lecture, 04:20-05:43, at https://youtu.be/NlU1m-OfumE

    Examples
    --------
    Make a mesh from scratch
    >>> from compas.geometry import Sphere
    >>> sphere = Sphere([1, 1, 1], 1)
    >>> sphere = Mesh.from_shape(sphere, u=30, v=30)
    >>> sphere.quads_to_triangles()
    >>> M = sphere.to_vertices_and_faces()

    Compute the discrete mean curvature
    >>> H = trimesh_mean_curvature(M)

    """
    # (0) see if input is already Rhino.Geometry.Mesh
    mesh = Rhino.Geometry.Mesh()
    if not isinstance(M, Rhino.Geometry.Mesh):
        for x, y, z in M[0]:
            mesh.Vertices.Add(x, y, z)
        for face in M[1]:
            mesh.Faces.AddFace(*face)
    else:
        mesh = M

    # (1) check if it is a trimesh
    if mesh.Faces.QuadCount > 0:
        raise ValueError("Mesh is not trimesh.")

    # (2) Prepare ingredient and return list
    H = []
    mesh.FaceNormals.ComputeFaceNormals()
    faces_normal = mesh.FaceNormals

    # (3) Main - loop over all vertices
    for i in range(mesh.Vertices.Count):
        edges = mesh.TopologyVertices.ConnectedEdges(mesh.TopologyVertices.TopologyVertexIndex(i))
        vertex = FromPoint3f(mesh.Vertices[i])
        if edges is None:
            H.append(None)
            continue
        x = []
        # (3.1) loop topology edges of such vertex
        for edge in edges:
            l_ij = mesh.TopologyEdges.EdgeLine(edge).Length
            orientation = StrongBox[Array[bool]]()
            faces = mesh.TopologyEdges.GetConnectedFaces(edge, orientation)
            if len(faces) != 2:
                x.append(0)
                continue
            # (3.2) to know which face is on left or right
            orientation = list(orientation.Value)
            start_pt = mesh.TopologyEdges.EdgeLine(edge).From
            direction = start_pt.EpsilonEquals(vertex, doc.ModelAbsoluteTolerance)
            normals = dict(zip(orientation, [faces_normal[faces[0]], faces_normal[faces[1]]]))
            e = mesh.TopologyEdges.EdgeLine(edge).Direction
            e.Unitize()
            n1 = normals[True]
            n2 = normals[False]
            if not direction:
                e.Reverse()
                n1, n2 = n2, n1
            # (3.3) calculate dihedral angle
            angle = dihedral_angle(e, n1, n2)
            x.append(l_ij * angle)
        H.append(1/4 * sum(x))

    # (4) Output
    return H


@plugin(category="trimesh", requires=['Rhino'])
def trimesh_principal_curvature(M):
    r"""Compute the principal curvature of a triangle mesh.
    Parameters
    ----------
    M : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.

    Returns
    -------
    list[float]
        The max curvature per vertex.
    list[float]
        The min curvature per vertex.

    Notes
    -----
    Description: The discrete principal curvature is computed by mean curvature, Gaussian curvature, and vertex area.

    Notation Convention:
        * :math:`\kappa^1_i, \kappa^2_i` - The max principal curvature and the min principal curvature at the vertex i
        * :math:`H_i` - the discrete mean curvature at vertex i
        * :math:`K_i` - the discrete Gaussian curvature at vertex i
        * :math:`A_i` - the area of the dual cell centered at vertex i

    Formula:

    .. math::

        \kappa^1_i, \kappa^2_i =  \frac{H_i}{A_i}\pm\sqrt{\left( \,\frac{H_i}{A_i}\right)\,^2-\frac{K_i}{A_i}}

    References
    ----------
    .. [1] Formula of Discrete Principal Curvature available at Keenan Crane's lecture, 03:16-07:11, at https://youtu.be/sokeN5VxBB8

    Examples
    --------
    Make a mesh from scratch
    >>> from compas.geometry import Sphere
    >>> sphere = Sphere([1, 1, 1], 1)
    >>> sphere = Mesh.from_shape(sphere, u=30, v=30)
    >>> sphere.quads_to_triangles()
    >>> M = sphere.to_vertices_and_faces()

    Compute the discrete principal curvature
    >>> H = trimesh_principal_curvature(M)

    """
    # (0) see if input is already Rhino.Geometry.Mesh
    mesh = Rhino.Geometry.Mesh()
    if not isinstance(M, Rhino.Geometry.Mesh):
        for x, y, z in M[0]:
            mesh.Vertices.Add(x, y, z)
        for face in M[1]:
            mesh.Faces.AddFace(*face)
    else:
        mesh = M

    # (1) check if it is a trimesh
    if mesh.Faces.QuadCount > 0:
        raise ValueError("Mesh is not trimesh.")

    # (2) Prepare ingredient and return list
    k1 = []
    k2 = []
    A = trimesh_barycentric_area(mesh)
    H = trimesh_mean_curvature(mesh)
    K = trimesh_gaussian_curvature(mesh)

    # (3) Main - loop over all vertices
    for i in range(mesh.Vertices.Count):
        if(A[i] == 0):
            k1.append(None)
            k2.append(None)
            continue
        H_ = H[i] / A[i]
        K_ = K[i] / A[i]

        discriminant = sqrt(max(0, H_ * H_ - K_))
        k1.append(H_ + discriminant)
        k2.append(H_ - discriminant)

    # (4) Output
    return k1, k2


def trimesh_barycentric_area(mesh):
    # (1) prepare return list
    areas = [0] * mesh.Vertices.Count

    # (2) Main - loop over mesh faces rather than vertices
    for i in range(mesh.Faces.Count):
        # Heron's Formula
        ptA = mesh.Faces.GetFaceVertices(i)[1]
        ptB = mesh.Faces.GetFaceVertices(i)[2]
        ptC = mesh.Faces.GetFaceVertices(i)[3]
        dA = ptB.DistanceTo(ptC)
        dB = ptA.DistanceTo(ptC)
        dC = ptA.DistanceTo(ptB)
        p = (dA+dB+dC)/2
        area = sqrt(p*(p-dA)*(p-dB)*(p-dC)) / 3

        # topology vertices
        verts_topo = mesh.Faces.GetTopologicalVertices(i)
        # vertices
        verts = []
        for vert_topo in verts_topo:
            verts.extend(mesh.TopologyVertices.MeshVertexIndices(vert_topo))
        for vert in verts:
            areas[vert] += area

    # (3) output
    return areas


def dihedral_angle(e, n1, n2):
    # Compute the dihedral angle of an edge
    # e: the vector from vertex i to j
    # n1: the normal vector of MeshFace on LEFT side
    # n2: the normal vector of MeshFace on RIGHT side

    cos_theta = Multiply(n1, n2)
    sin_theta = Multiply(CrossProduct(n1, n2), e)
    return atan2(sin_theta, cos_theta)


# src/compas_rhino/geometry/trimesh/slice.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import Rhino
from Rhino.Geometry import Point3d, Vector3d, Plane
from Rhino.Geometry.Intersect.Intersection import MeshPlane

from compas.plugins import plugin


__all__ = [
    'trimesh_slice',
]


@plugin(category="trimesh", requires=['Rhino'])
def trimesh_slice(mesh, planes):
    """Slice a mesh by a list of planes.

    Parameters
    ----------
    mesh : tuple[sequence[[float, float, float] or :class:`compas.geometry.Point`], sequence[[int, int, int]]]
        The mesh to slice.
    planes : sequence[[point, vector] or :class:`compas.geometry.Plane`]
        The slicing planes.

    Returns
    -------
    list[list[[float, float, float]]]
        The points defining the slice polylines.

    Examples
    --------
    >>> from compas.geometry import Sphere, Plane, Point, Vector
    >>> sphere = Sphere([1, 1, 1], 1)
    >>> sphere = sphere.to_vertices_and_faces(u=30, v=30)
    >>> P1 = Plane(Point(0, 0, 0), Vector(0, -1, 0))
    >>> P2 = Plane(Point(0, 0, 0), Vector(0.87, -0.5, 0))
    >>> planes = [P1, P2]
    >>> points = trimesh_slice(sphere, planes)

    """
    # (0) see if input is already Rhino.Geometry.Mesh
    M = Rhino.Geometry.Mesh()
    if not isinstance(mesh, Rhino.Geometry.Mesh):
        for x, y, z in mesh[0]:
            M.Vertices.Add(x, y, z)
        for face in mesh[1]:
            M.Faces.AddFace(*face)
    else:
        M = mesh
    # (1) parse to Rhino.Geometry.Plane
    P = []
    for plane in planes:
        point = Point3d(plane[0][0], plane[0][1], plane[0][2])
        normal = Vector3d(plane[1][0], plane[1][1], plane[1][2])
        P.append(Plane(point, normal))
    # (2) Slice
    polylines = MeshPlane(M, P)
    # (3) Return points in a list of arrays
    polyline_pts = []
    for polyline in polylines:
        pts = []
        for i in range(polyline.Count):
            pts.append([polyline.X[i], polyline.Y[i], polyline.Z[i]])
        polyline_pts.append(pts)

    return polyline_pts


# src/compas_rhino/__init__.py

"""
************
compas_rhino
************

.. currentmodule:: compas_rhino

.. toctree::
    :maxdepth: 1

    compas_rhino.artists
    compas_rhino.conduits
    compas_rhino.conversions
    compas_rhino.forms
    compas_rhino.geometry
    compas_rhino.utilities

"""
from __future__ import absolute_import

import io
import os

import compas
import compas._os

if compas.is_rhino():
    import rhinoscriptsyntax as rs  # noqa: F401
    from .utilities import *  # noqa: F401 F403


__version__ = '1.13.3'


PURGE_ON_DELETE = True

INSTALLABLE_PACKAGES = ['compas', 'compas_rhino', 'compas_ghpython']
SUPPORTED_VERSIONS = ['5.0', '6.0', '7.0', '8.0']
DEFAULT_VERSION = '7.0'

IRONPYTHON_PLUGIN_GUID = '814d908a-e25c-493d-97e9-ee3861957f49'
GRASSHOPPER_PLUGIN_GUID = 'b45a29b1-4343-4035-989e-044e8580d9cf'
RHINOCYCLES_PLUGIN_GUID = '9bc28e9e-7a6c-4b8f-a0c6-3d05e02d1b97'


__all__ = [
    'PURGE_ON_DELETE',
    'INSTALLABLE_PACKAGES',
    'SUPPORTED_VERSIONS',
    'DEFAULT_VERSION',
    'IRONPYTHON_PLUGIN_GUID',
    'GRASSHOPPER_PLUGIN_GUID',
    'RHINOCYCLES_PLUGIN_GUID',

    'clear',
    'redraw'
]

__all_plugins__ = [
    'compas_rhino.geometry.booleans',
    'compas_rhino.geometry.trimesh',
    'compas_rhino.install',
    'compas_rhino.uninstall',
    'compas_rhino.artists',
    'compas_rhino.geometry.curves',
    'compas_rhino.geometry.surfaces',
]


# =============================================================================
# General helpers
# =============================================================================


def clear():
    guids = get_objects()  # noqa: F405
    delete_objects(guids, purge=True)  # noqa: F405


def redraw():
    rs.EnableRedraw(True)
    rs.Redraw()


def _check_rhino_version(version):
    if not version:
        return DEFAULT_VERSION

    if version not in SUPPORTED_VERSIONS:
        raise Exception('Unsupported Rhino version: {}'.format(version))

    return version


def _get_package_path(package):
    return os.path.abspath(os.path.dirname(package.__file__))


def _get_bootstrapper_path(install_path):
    return os.path.join(install_path, 'compas_bootstrapper.py')


def _get_bootstrapper_data(compas_bootstrapper):
    data = {}

    if not os.path.exists(compas_bootstrapper):
        return data

    content = io.open(compas_bootstrapper, encoding='utf8').read()
    exec(content, data)

    return data


def _try_remove_bootstrapper(path):
    """Try to remove bootstrapper.

    Returns
    -------
    bool: True if the operation did not cause errors, False otherwise.
    """

    bootstrapper = _get_bootstrapper_path(path)

    if os.path.exists(bootstrapper):
        try:
            os.remove(bootstrapper)
            return True
        except:  # noqa: E722
            return False
    return True


# =============================================================================
# Base Application folder (Program Files on Windows and Applications on Mac)
# =============================================================================


def _get_rhino_application_folder(version):
    version = _check_rhino_version(version)
    version = version.split('.')[0]   # take the major only

    if compas.WINDOWS:
        app = os.path.join(os.getenv('ProgramFiles'), 'Rhino {}'.format(version))
    elif compas.OSX:
        paths = {
            '5': ['/', 'Applications', 'Rhinoceros.app'],
            '6': ['/', 'Applications', 'Rhinoceros.app'],
            '7': ['/', 'Applications', 'Rhino 7.app', ],
            '8': ['/', 'Applications', 'RhinoWIP.app', ]
        }
        app = os.path.join(*paths[version])

    else:
        raise Exception('Unsupported platform')

    if not os.path.exists(app):
        raise Exception("The application folder does not exist in this location: {}".format(app))

    return app


# =============================================================================
# Base AppData folder (APPDATA on Windows and Application Support on Mac)
# =============================================================================


def _get_rhino_appdata_folder():
    if compas.WINDOWS:
        app = os.path.join(os.getenv('APPDATA'), 'McNeel', 'Rhinoceros')

    elif compas.OSX:
        app = os.path.join(os.getenv('HOME'), 'Library', 'Application Support', 'McNeel', 'Rhinoceros')

    else:
        raise Exception('Unsupported platform')

    if not os.path.exists(app):
        raise Exception("The appdata folder does not exist in this location: {}".format(app))

    return app


# =============================================================================
# Scripts folder
# =============================================================================


def _get_rhino_scripts_path(version):
    appdata = _get_rhino_appdata_folder()
    version = _check_rhino_version(version)
    scripts_path = os.path.join(appdata, '{}'.format(version), 'scripts')

    if not os.path.exists(scripts_path):
        raise Exception("The scripts folder does not exist in this location: {}".format(scripts_path))

    return scripts_path


# =============================================================================
# Managed Plugins folder
# =============================================================================


def _get_rhino_managedplugins_path(version):
    app = _get_rhino_application_folder(version)

    if compas.WINDOWS:
        managedplugins_path = os.path.join(app, 'Plug-ins')

    elif compas.OSX:
        managedplugins_path = os.path.join(app, 'Contents', 'Frameworks', 'RhCore.framework', 'Versions', 'A', 'Resources', 'ManagedPlugIns')

    if not os.path.exists(managedplugins_path):
        raise Exception("The Managed Plug-ins folder does not exist in this location: {}".format(managedplugins_path))

    return managedplugins_path


# =============================================================================
# Plugins folder
# =============================================================================


def _get_rhino_plugins_path(version):
    appdata = _get_rhino_appdata_folder()
    version = _check_rhino_version(version)

    if compas.WINDOWS:
        plugins_path = os.path.join(appdata, '{}'.format(version), 'Plug-ins')

    elif compas.OSX:
        if version == '5.0':
            plugins_path = os.path.join(appdata, 'MacPlugIns')
        else:
            plugins_path = os.path.join(appdata, '{}'.format(version), 'Plug-ins')

    if not os.path.exists(plugins_path):
        raise Exception("The plugins folder does not exist in this location: {}".format(plugins_path))

    return plugins_path


# =============================================================================
# PythonPlugins folder
# =============================================================================


def _get_rhino_pythonplugins_path(version):
    version = _check_rhino_version(version)
    return os.path.join(_get_rhino_plugins_path(version), 'PythonPlugins')


# =============================================================================
# IronPython Plugin
# =============================================================================


def _get_rhino_ironpythonplugin_path(version):
    version = _check_rhino_version(version)
    return os.path.join(_get_rhino_plugins_path(version), 'IronPython ({})'.format(IRONPYTHON_PLUGIN_GUID))


# =============================================================================
# Grasshopper
# =============================================================================


def _get_rhino_grasshopperplugin_path(version):
    version = _check_rhino_version(version)

    if compas.WINDOWS:
        gh_path = os.path.join(os.getenv('APPDATA'), 'Grasshopper')

    elif compas.OSX:
        gh_path = os.path.join(_get_rhino_plugins_path(version), 'Grasshopper ({})'.format(GRASSHOPPER_PLUGIN_GUID))

    else:
        raise Exception('Unsupported platform')

    if not os.path.exists(gh_path):
        raise Exception("The grasshopper folder does not exist in this location: {}".format(gh_path))

    return gh_path


# =============================================================================
# IronPython lib folder
# =============================================================================


def _get_rhino_ironpython_lib_path(version):
    version = _check_rhino_version(version)

    if compas.WINDOWS:
        ipy_lib_path = _get_rhino_ironpython_lib_path_win32(version)

    elif compas.OSX:
        ipy_lib_path = _get_rhino_ironpython_lib_path_mac(version)

    if not os.path.exists(ipy_lib_path):
        ipy_lib_path = None
        # print("The lib folder for IronPython does not exist in this location: {}".format(ipy_lib_path))

    return ipy_lib_path


def _get_rhino_ironpython_lib_path_win32(version):
    return os.path.join(_get_rhino_ironpythonplugin_path(version), 'settings', 'lib')


# For 5.0 this is correct
# For +6 we should switch to the same path as on windows
# which is not in the managed plugins but in the appdata plugins
def _get_rhino_ironpython_lib_path_mac(version):
    return os.path.join(_get_rhino_managedplugins_path(version), 'RhinoDLR_Python.rhp', 'Lib')


# src/compas_rhino/install.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import importlib
import itertools
import os
import sys

import compas_rhino

import compas._os
import compas.plugins

__all__ = [
    'install',
    'installable_rhino_packages',
    'after_rhino_install'
]

INSTALLED_VERSION = None


def install(version=None, packages=None, clean=False):
    """Install COMPAS for Rhino.

    Parameters
    ----------
    version : {'5.0', '6.0', '7.0', '8.0'}, optional
        The version number of Rhino.
        Default is ``'7.0'``.
    packages : list of str, optional
        List of packages to install or None to use default package list.
        Default is the result of ``installable_rhino_packages``,
        which collects all installable packages in the current environment.
    clean : bool, optional
        If True, this will clean up the entire scripts folder and remove
        also existing symlinks that are not importable in the current environment.

    Examples
    --------
    .. code-block:: python

        import compas_rhino.install
        compas_rhino.install.install()

    .. code-block:: bash

        python -m compas_rhino.install

    """
    version = compas_rhino._check_rhino_version(version)

    # We install COMPAS packages in the scripts folder
    # instead of directly as IPy module.
    scripts_path = compas_rhino._get_rhino_scripts_path(version)

    # This is for old installs
    ipylib_path = compas_rhino._get_rhino_ironpython_lib_path(version)

    # Filter the provided list of packages
    # If no packages are provided
    # this first collects all installable packages from the environment.
    packages = _filter_installable_packages(version, packages)

    results = []
    symlinks_to_install = []
    symlinks_to_uninstall = []
    exit_code = 0

    # check all installable packages
    # add the packages that can't be imported from the current env to the list of symlinks to uninstall
    # and remove the package name from the list of installable packages
    # make a copy of the list to avoid problems with removing items
    # note: perhaps this should already happen in the filter function...
    for name in packages[:]:
        try:
            importlib.import_module(name)
        except ImportError:
            path = os.path.join(scripts_path, name)
            symlinks_to_uninstall.append(dict(name=name, link=path))
            packages.remove(name)

    # Also remove all broken symlinks from the scripts folder
    # because ... they're broken!
    # If it is an actual folder or a file, leave it alone
    # because probably someone put it there on purpose.
    for name in os.listdir(scripts_path):
        path = os.path.join(scripts_path, name)
        if os.path.islink(path):
            if not os.path.exists(path):
                symlinks_to_uninstall.append(dict(name=name, link=path))
                try:
                    importlib.import_module(name)
                except ImportError:
                    pass
                else:
                    if name not in packages:
                        packages.append(name)

    # If the scripts folder is supposed to be cleaned
    # also remove all existing symlinks that cannot be imported
    # and reinstall symlinks that can be imported
    if clean:
        for name in os.listdir(scripts_path):
            path = os.path.join(scripts_path, name)
            if os.path.islink(path):
                if os.path.exists(path):
                    try:
                        importlib.import_module(name)
                    except ImportError:
                        path = os.path.join(scripts_path, name)
                        symlinks_to_uninstall.append(dict(name=name, link=path))
                    else:
                        if name not in packages:
                            packages.append(name)

    # add all of the packages in the list of installable packages
    # to the list of symlinks to uninstall
    # and to the list of symlinks to install
    for package in packages:
        symlink_path = os.path.join(scripts_path, package)
        symlinks_to_uninstall.append(dict(name=package, link=symlink_path))

        package_path = compas_rhino._get_package_path(importlib.import_module(package))
        symlinks_to_install.append(dict(name=package, source_path=package_path, link=symlink_path))

        # Handle legacy install location
        # This does not always work,
        # and especially not in cases where it is not necessary :)
        if ipylib_path:
            legacy_path = os.path.join(ipylib_path, package)
            if os.path.exists(legacy_path):
                symlinks_to_uninstall.append(dict(name=package, link=legacy_path))

    # -------------------------
    # Uninstall first
    # -------------------------

    symlinks = [link['link'] for link in symlinks_to_uninstall]
    uninstall_results = compas._os.remove_symlinks(symlinks)

    # Let the user know if some symlinks could not be removed.
    for uninstall_data, success in zip(symlinks_to_uninstall, uninstall_results):
        if not success:
            results.append((uninstall_data['name'], 'ERROR: Cannot remove symlink, try to run as administrator.'))

    # Handle legacy bootstrapper
    # Again, only if possible...
    if ipylib_path:
        if not compas_rhino._try_remove_bootstrapper(ipylib_path):
            results.append(('compas_bootstrapper', 'ERROR: Cannot remove legacy compas_bootstrapper, try to run as administrator.'))

    # -------------------------
    # Ready to start installing
    # -------------------------

    # create new symlinks and register the results
    symlinks = [(link['source_path'], link['link']) for link in symlinks_to_install]
    install_results = compas._os.create_symlinks(symlinks)

    # set the exit code based on the installation results
    if not all(install_results):
        exit_code = -1

    # make a list of installed packages
    # based on the installation results
    # and update the general results list
    installed_packages = []
    for install_data, success in zip(symlinks_to_install, install_results):
        if success:
            installed_packages.append(install_data['name'])
            result = 'OK'
        else:
            result = 'ERROR: Cannot create symlink, try to run as administrator.'
        results.append((install_data['name'], result))

    # finalize the general results list with info about the bootstrapper
    if exit_code == -1:
        results.append(('compas_bootstrapper', 'WARNING: One or more packages failed, will not install bootstrapper, try uninstalling first'))
    else:
        try:
            _update_bootstrapper(scripts_path, packages)
            results.append(('compas_bootstrapper', 'OK'))
        except:  # noqa: E722
            results.append(('compas_bootstrapper', 'ERROR: Could not create compas_bootstrapper to auto-determine Python environment'))

    # output the outcome of the installation process
    # perhaps we should more info here
    print('Installing COMPAS packages to Rhino {0} scripts folder:'.format(version))
    print('{}\n'.format(scripts_path))

    for package, status in results:
        print('   {} {}'.format(package.ljust(20), status))
        if status != 'OK':
            exit_code = -1

    if exit_code == 0 and len(installed_packages):
        print('\nRunning post-installation steps...\n')
        if not _run_post_execution_steps(after_rhino_install(installed_packages)):
            exit_code = -1

    print('\nInstall completed.')
    if exit_code != 0:
        sys.exit(exit_code)

    global INSTALLED_VERSION
    INSTALLED_VERSION = version


def _run_post_execution_steps(steps_generator):
    all_steps_succeeded = True
    post_execution_errors = []

    for result in steps_generator:
        if isinstance(result, Exception):
            post_execution_errors.append(result)
            continue

        for item in result:
            try:
                package, message, success = item
                status = 'OK' if success else 'ERROR'
                if not success:
                    all_steps_succeeded = False
                print('   {} {}: {}'.format(package.ljust(20), status, message))
            except ValueError:
                post_execution_errors.append(ValueError('Step ran without errors but result is wrongly formatted: {}'.format(str(item))))

    if post_execution_errors:
        print('\nOne or more errors occurred:\n')
        for error in post_execution_errors:
            print('   - {}'.format(repr(error)))

        all_steps_succeeded = False

    return all_steps_succeeded


@compas.plugins.plugin(category='install', pluggable_name='installable_rhino_packages', tryfirst=True)
def default_installable_rhino_packages():
    # While this list could obviously be hard-coded, I think
    # eating our own dogfood and using plugins to define this, just like
    # any other extension/plugin would be is a better way to ensure consistent behavior.
    return ['compas', 'compas_rhino']


@compas.plugins.pluggable(category='install', selector='collect_all')
def installable_rhino_packages():
    """Provide a list of packages to make available inside Rhino.

    Extensions providing Rhino or Grasshopper features
    can implement this pluggable interface to automatically
    have their packages made available inside Rhino when
    COMPAS is installed into it.

    Examples
    --------
    >>> import compas.plugins
    >>> @compas.plugins.plugin(category='install')
    ... def installable_rhino_packages():
    ...    return ['compas_fab']

    Returns
    -------
    :obj:`list` of :obj:`str`
        List of package names to make available inside Rhino.
    """
    pass


@compas.plugins.pluggable(category='install', selector='collect_all')
def after_rhino_install(installed_packages):
    """Allows extensions to execute actions after install to Rhino is done.

    Extensions providing Rhino or Grasshopper features
    can implement this pluggable interface to perform
    additional steps after an installation to Rhino has
    been completed.

    Parameters
    ----------
    installed_packages : :obj:`list` of :obj:`str`
        List of packages that have been installed successfully.

    Examples
    --------
    >>> import compas.plugins
    >>> @compas.plugins.plugin(category='install')
    ... def after_rhino_install(installed_packages):
    ...    # Do something after package is installed to Rhino, eg, copy components, etc
    ...    return [('compas_ghpython', 'GH Components installed', True)]

    Returns
    -------
    :obj:`list` of 3-tuple (str, str, bool)
        List containing a 3-tuple with component name, message and True/False success flag.
    """
    pass


def _update_bootstrapper(install_path, packages):
    # Take either the CONDA environment directory or the current Python executable's directory
    python_directory = os.environ.get('CONDA_PREFIX', None) or os.path.dirname(sys.executable)
    environment_name = os.environ.get('CONDA_DEFAULT_ENV', '')
    conda_exe = os.environ.get('CONDA_EXE', '')

    compas_bootstrapper = compas_rhino._get_bootstrapper_path(install_path)

    bootstrapper_data = compas_rhino._get_bootstrapper_data(compas_bootstrapper)
    installed_packages = bootstrapper_data.get('INSTALLED_PACKAGES', [])
    installed_packages = list(set(installed_packages + list(packages)))

    with open(compas_bootstrapper, 'w') as f:
        f.write('ENVIRONMENT_NAME = r"{}"\n'.format(environment_name))
        f.write('PYTHON_DIRECTORY = r"{}"\n'.format(python_directory))
        f.write('CONDA_EXE = r"{}"\n'.format(conda_exe))
        f.write('INSTALLED_PACKAGES = {}'.format(repr(installed_packages)))


def _filter_installable_packages(version, packages):
    ghpython_incompatible = False

    if compas.OSX and version == 5.0:
        ghpython_incompatible = True

    if not packages:
        # Flatten list of results (resulting from collect_all pluggable)
        packages = sorted(set(itertools.chain.from_iterable(installable_rhino_packages())))
    elif 'compas_ghpython' in packages and ghpython_incompatible:
        print('Skipping installation of compas_ghpython since it\'s not supported for Rhino 5 for Mac')

    if ghpython_incompatible:
        packages.remove('compas_ghpython')

    return packages


# ==============================================================================
# Main
# ==============================================================================

if __name__ == "__main__":

    import argparse

    parser = argparse.ArgumentParser()

    parser.add_argument(
        '-v',
        '--version',
        choices=compas_rhino.SUPPORTED_VERSIONS,
        default=compas_rhino.DEFAULT_VERSION,
        help="The version of Rhino to install the packages in."
    )
    parser.add_argument('-p', '--packages', nargs='+', help="The packages to install.")
    parser.add_argument('--clean', dest='clean', default=False, action='store_true')

    args = parser.parse_args()

    install(version=args.version, packages=args.packages, clean=args.clean)


# src/compas_rhino/install_plugin.py

import os
import imp

import compas_rhino

from compas._os import create_symlinks
from compas._os import remove_symlinks


__all__ = ['install_plugin']


def install_plugin(plugin, version=None):
    """Install a Rhino Python Command Plugin.

    Parameters
    ----------
    plugin : str
        The path to the plugin folder.
    version : {'5.0', '6.0', '7.0', '8.0'}, optional
        The version of Rhino for which the plugin should be installed.
        Default is ``'6.0'``.

    Notes
    -----
    The version info is only relevant for Rhino on Windows.

    The function creates an *editable install*, which means that the plugin
    folder is *symlinked* into the correct location so Rhino can find and load it.
    The contents of the source folder can still be edited and next time you start
    Rhino those canges will be reflected in the loaded plugin.

    Examples
    --------
    Assuming the plugin folder is at the following location on your computer

    .. code-block:: none

        ~/Code/compas_xxx/ui/Rhino/XXX

    and contains at least a "dev" folder with a ``__plugin__.py`` file with

    * the GUID of the tool: ``id={...}``, and
    * the name of the tool: ``title="..."``,

    it can be installed with the following command,

    .. code-block:: bash

        cd ~/Code/compas_xxx
        python -m compas_rhino.install_plugin ui/Rhino/XXX

    or the following, if the plugin should be installed for Rhino 7.

    .. code-block:: bash

        cd ~/Code/compas_xxx
        python -m compas_rhino.install_plugin -v 7.0 ui/Rhino/XXX

    """
    if not os.path.isdir(plugin):
        raise Exception('Cannot find the plugin: {}'.format(plugin))

    version = compas_rhino._check_rhino_version(version)

    plugin_dir = os.path.abspath(plugin)

    # Clean up the plugin directory
    # by removing all broken symlinks
    # because ... they're broken!

    symlinks_to_remove = []

    for name in os.listdir(plugin_dir):
        path = os.path.join(plugin_dir, name)
        if os.path.islink(path):
            if not os.path.exists(path):
                symlinks_to_remove.append(dict(name=name, link=path))

    symlinks_removed = []
    symlinks_unremoved = []

    if symlinks_to_remove:
        symlinks = [link['link'] for link in symlinks_to_remove]
        removal_results = remove_symlinks(symlinks)
        for uninstall_data, success in zip(symlinks_to_remove, removal_results):
            if success:
                symlinks_removed.append(uninstall_data['name'])
            else:
                symlinks_unremoved.append(uninstall_data['name'])

    if symlinks_removed:
        print('\nThe following package symlinks are broken and were removed:\n')
        for name in symlinks_removed:
            print('   {}'.format(name.ljust(20)))

    if symlinks_unremoved:
        print('\nThe following package symlinks are broken but could not be removed:\n')
        for name in symlinks_unremoved:
            print('   {}'.format(name.ljust(20)))

    # -----------------------------
    # proceed with the installation
    # -----------------------------

    plugin_path, plugin_name = os.path.split(plugin_dir)
    plugin_path = plugin_path or os.getcwd()

    plugin_dev = os.path.join(plugin_dir, 'dev')

    if not os.path.isdir(plugin_dev):
        raise Exception('The plugin does not contain a dev folder.')

    plugin_info = os.path.join(plugin_dev, '__plugin__.py')

    if not os.path.isfile(plugin_info):
        raise Exception('The plugin does not contain plugin info.')

    __plugin__ = imp.load_source('__plugin__', plugin_info)

    if not __plugin__.id:
        raise Exception('Plugin id is not set.')

    if not __plugin__.title:
        raise Exception('Plugin title is not set.')

    plugin_fullname = "{}{}".format(__plugin__.title, __plugin__.id)

    python_plugins_path = compas_rhino._get_rhino_pythonplugins_path(version)

    if not os.path.exists(python_plugins_path):
        os.mkdir(python_plugins_path)

    source = plugin_dir
    destination = os.path.join(python_plugins_path, plugin_fullname)

    print('\nInstalling PlugIn {} to Rhino PythonPlugIns.'.format(plugin_name))

    remove_symlinks([destination])
    create_symlinks([(source, destination)])

    print('\nPlugIn {} Installed.'.format(plugin_name))
    print('\nRestart Rhino and open the Python editor at least once to make it available.')


# ==============================================================================
# Main
# ==============================================================================

if __name__ == "__main__":

    import argparse

    parser = argparse.ArgumentParser(
        description='COMPAS Rhino Plugin Installation command-line utility.')

    parser.add_argument('plugin', help="The path to the plugin directory.")
    parser.add_argument(
        '-v',
        '--version',
        choices=compas_rhino.SUPPORTED_VERSIONS,
        default=compas_rhino.DEFAULT_VERSION,
        help="The version of Rhino."
    )

    args = parser.parse_args()

    install_plugin(args.plugin, version=args.version)


# src/compas_rhino/uninstall.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import itertools
import os
import sys

import compas._os
import compas.plugins
import compas_rhino
from compas_rhino.install import _run_post_execution_steps
from compas_rhino.install import installable_rhino_packages

__all__ = [
    'uninstall',
    'after_rhino_uninstall',
]


def uninstall(version=None, packages=None):
    """Uninstall COMPAS from Rhino.

    Parameters
    ----------
    version : {'5.0', '6.0', '7.0', '8.0'}, optional
        The version number of Rhino.
        Default is ``'7.0'``.
    packages : list of str, optional
        List of packages to uninstall.
        Default is to uninstall all packages installed by the COMPAS installer.

    Examples
    --------
    .. code-block:: python

        import compas_rhino
        compas_rhino.uninstall()

    .. code-block:: bash

        python -m compas_rhino.uninstall

    """
    version = compas_rhino._check_rhino_version(version)

    # We install COMPAS packages in the scripts folder
    # instead of directly as IPy module.
    scripts_path = compas_rhino._get_rhino_scripts_path(version)

    # This is for old installs
    ipylib_path = compas_rhino._get_rhino_ironpython_lib_path(version)

    # Filter the provided list of packages
    # If no packages are provided
    # this first collects all installable packages from the environment.
    packages = _filter_installed_packages(version, packages)

    # Also remove all broken symlinks
    # because ... they're broken!
    for name in os.listdir(scripts_path):
        path = os.path.join(scripts_path, name)
        if os.path.islink(path):
            if not os.path.exists(path):
                if name not in packages:
                    packages.append(name)

    # Collect paths for removal based on package names
    symlinks_to_uninstall = []

    for package in packages:
        symlink_path = os.path.join(scripts_path, package)
        symlinks_to_uninstall.append(dict(name=package, link=symlink_path))

        # Handle legacy install location
        # This does not always work,
        # and especially not in cases where it is in any case not necessary :)
        if ipylib_path:
            legacy_path = os.path.join(ipylib_path, package)
            if os.path.exists(legacy_path):
                symlinks_to_uninstall.append(dict(name=package, link=legacy_path))

    # There is nothing to uninstall
    if not symlinks_to_uninstall:
        print('\nNo packages to uninstall from Rhino {0} scripts folder: \n{1}.'.format(version, scripts_path))
        return

    # -------------------------
    # Start uninstalling
    # -------------------------

    uninstalled_packages = []
    results = []
    exit_code = 0

    symlinks = [link['link'] for link in symlinks_to_uninstall]
    uninstall_results = compas._os.remove_symlinks(symlinks)

    for uninstall_data, success in zip(symlinks_to_uninstall, uninstall_results):
        if success:
            uninstalled_packages.append(uninstall_data['name'])
            result = 'OK'
        else:
            result = 'ERROR: Cannot remove symlink, try to run as administrator.'

        results.append((uninstall_data['name'], result))

    if not all(uninstall_results):
        exit_code = -1

    if exit_code == -1:
        results.append(('compas_bootstrapper', 'WARNING: One or more packages failed, will not uninstall bootstrapper.'))

    else:
        if compas_rhino._try_remove_bootstrapper(scripts_path):
            results.append(('compas_bootstrapper', 'OK'))
        else:
            results.append(('compas_bootstrapper', 'ERROR: Cannot remove compas_bootstrapper, try to run as administrator.'))

        # Handle legacy bootstrapper
        # Again, only if possible...
        if ipylib_path:
            if not compas_rhino._try_remove_bootstrapper(ipylib_path):
                results.append(('compas_bootstrapper', 'ERROR: Cannot remove legacy compas_bootstrapper, try to run as administrator.'))

    # -------------------------
    # Output results
    # -------------------------

    print('Uninstalling COMPAS packages from Rhino {0} scripts folder: \n{1}'.format(version, scripts_path))
    print('\nThe following packages have been detected and will be uninstalled:\n')

    for package, status in results:
        print('   {} {}'.format(package.ljust(20), status))

        if status != 'OK':
            exit_code = -1

    if exit_code == 0 and uninstalled_packages:
        print('\nRunning post-uninstallation steps...\n')

        if not _run_post_execution_steps(after_rhino_uninstall(uninstalled_packages)):
            exit_code = -1

    print('\nUninstall completed.')

    if exit_code != 0:
        sys.exit(exit_code)


def _filter_installed_packages(version, packages):
    ipylib_path = compas_rhino._get_rhino_ironpython_lib_path(version)
    scripts_path = compas_rhino._get_rhino_scripts_path(version)

    compas_bootstrapper = compas_rhino._get_bootstrapper_path(scripts_path)
    bootstrapper_data = compas_rhino._get_bootstrapper_data(compas_bootstrapper)

    # Don't modify the original list if we have one
    if packages:
        packages = packages[:]
    else:
        packages = bootstrapper_data.get('INSTALLED_PACKAGES', None)

        # No info, fall back to installable packages list
        if packages is None:
            packages = list(itertools.chain.from_iterable(installable_rhino_packages()))

    # Handle legacy install
    if ipylib_path:
        legacy_bootstrapper = compas_rhino._get_bootstrapper_path(ipylib_path)
        if os.path.exists(legacy_bootstrapper):
            bootstrapper_data = compas_rhino._get_bootstrapper_data(legacy_bootstrapper)
            legacy_packages = bootstrapper_data.get('INSTALLED_PACKAGES', None)

            if legacy_packages:
                packages.extend(legacy_packages)

    return packages


@compas.plugins.pluggable(category='install', selector='collect_all')
def after_rhino_uninstall(uninstalled_packages):
    """Allows extensions to execute actions after uninstall from Rhino is done.

    Extensions providing Rhino or Grasshopper features
    can implement this pluggable interface to perform
    additional steps after the uninstall from Rhino has
    been completed.

    Parameters
    ----------
    uninstalled_packages : :obj:`list` of :obj:`str`
        List of packages that have been uninstalled.

    Examples
    --------
    >>> import compas.plugins
    >>> @compas.plugins.plugin(category='install')
    ... def after_rhino_uninstall(uninstalled_packages):
    ...    # Do something cleanup, eg remove copied files.
    ...    return [('compas_ghpython', 'GH Components uninstalled', True)]

    Returns
    -------
    :obj:`list` of 3-tuple (str, str, bool)
        List containing a 3-tuple with component name, message and True/False success flag.
    """
    pass


# ==============================================================================
# Main
# ==============================================================================

if __name__ == "__main__":

    import argparse

    parser = argparse.ArgumentParser()

    parser.add_argument(
        '-v',
        '--version',
        choices=compas_rhino.SUPPORTED_VERSIONS,
        default=compas_rhino.DEFAULT_VERSION,
        help="The version of Rhino to install the packages in."
    )
    parser.add_argument('-p', '--packages', nargs='+', help="The packages to uninstall.")

    args = parser.parse_args()

    uninstall(version=args.version, packages=args.packages)


# src/compas_rhino/uninstall_plugin.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import os

import compas_rhino

from compas._os import remove_symlink


__all__ = ['uninstall_plugin']


def uninstall_plugin(plugin, version=None):
    """Uninstall a Rhino Python Command Plugin.

    Parameters
    ----------
    plugin : str
        The name of the plugin.
    version : {'5.0', '6.0', '7.0', '8.0'}, optional
        The version of Rhino for which the plugin should be uninstalled.
        Default is ``'7.0'``.

    Notes
    -----
    The version info is only relevant for Rhino on Windows.

    Examples
    --------
    .. code-block:: bash

        python -m compas_rhino.uninstall_plugin XXX

    """
    version = compas_rhino._check_rhino_version(version)

    python_plugins_path = compas_rhino._get_rhino_pythonplugins_path(version)
    plugin_name = plugin.split('{')[0]

    symlinks = []
    dirs = []

    for name in os.listdir(python_plugins_path):
        path = os.path.join(python_plugins_path, name)

        if os.path.islink(path):
            if name.split('{')[0] == plugin_name:
                symlinks.append(name)

        elif os.path.isdir(path):
            if name.split('{')[0] == plugin_name:
                dirs.append(name)

    print('\nUninstalling PlugIn {} from Rhino PythonPlugIns:'.format(plugin_name))

    if not symlinks and not dirs:
        print('Nothing to uninstall...\n')

    else:
        for name in symlinks:
            print('- {}'.format(name))
            destination = os.path.join(python_plugins_path, name)
            remove_symlink(destination)

        for name in dirs:
            print('- {}'.format(name))
            destination = os.path.join(python_plugins_path, name)
            os.rmdir(destination)

        print('\nPlugIn {} Uninstalled.\n'.format(plugin_name))


# ==============================================================================
# Main
# ==============================================================================

if __name__ == "__main__":

    import argparse

    parser = argparse.ArgumentParser()

    parser.add_argument('plugin', help="The name of the plugin.")
    parser.add_argument(
        '-v',
        '--version',
        choices=compas_rhino.SUPPORTED_VERSIONS,
        default=compas_rhino.DEFAULT_VERSION,
        help="The version of Rhino."
    )

    args = parser.parse_args()

    uninstall_plugin(args.plugin, version=args.version)


# src/compas_rhino/geometry/__init__.py

"""
********************************************************************************
geometry
********************************************************************************

.. currentmodule:: compas_rhino.geometry

Classes
=======

.. autosummary::
    :toctree: generated/
    :nosignatures:

    RhinoNurbsCurve
    RhinoNurbsSurface


Plugins
=======

Booleans
--------

.. autosummary::
    :toctree: generated/
    :nosignatures:

    booleans.boolean_difference_mesh_mesh
    booleans.boolean_intersection_mesh_mesh
    booleans.boolean_union_mesh_mesh

Curves
------

.. autosummary::
    :toctree: generated/
    :nosignatures:

    curves.new_nurbscurve
    curves.new_nurbscurve_from_interpolation
    curves.new_nurbscurve_from_parameters
    curves.new_nurbscurve_from_points
    curves.new_nurbscurve_from_step

TriMesh
-------

.. autosummary::
    :toctree: generated/
    :nosignatures:

    trimesh.trimesh_gaussian_curvature
    trimesh.trimesh_mean_curvature
    trimesh.trimesh_principal_curvature
    trimesh.trimesh_slice

"""
from __future__ import absolute_import

from compas_rhino.conversions import RhinoGeometry

from compas_rhino.conversions import RhinoBox
from compas_rhino.conversions import RhinoCircle
from compas_rhino.conversions import RhinoCone
from compas_rhino.conversions import RhinoCurve
from compas_rhino.conversions import RhinoCylinder
from compas_rhino.conversions import RhinoEllipse
from compas_rhino.conversions import RhinoLine
from compas_rhino.conversions import RhinoMesh
from compas_rhino.conversions import RhinoPlane
from compas_rhino.conversions import RhinoPoint
from compas_rhino.conversions import RhinoPolyline
from compas_rhino.conversions import RhinoSphere
from compas_rhino.conversions import RhinoSurface
from compas_rhino.conversions import RhinoVector

from .curves import RhinoNurbsCurve
from .surfaces import RhinoNurbsSurface


__all__ = [
    'RhinoGeometry',
    'RhinoBox',
    'RhinoCircle',
    'RhinoCone',
    'RhinoCurve',
    'RhinoCylinder',
    'RhinoEllipse',
    'RhinoLine',
    'RhinoMesh',
    'RhinoPlane',
    'RhinoPoint',
    'RhinoPolyline',
    'RhinoSphere',
    'RhinoSurface',
    'RhinoVector',

    'RhinoNurbsCurve',
    'RhinoNurbsSurface',
]


# src/compas_rhino/geometry/surfaces/__init__.py

from .nurbs import RhinoNurbsSurface

from compas.geometry import NurbsSurface
from compas.plugins import plugin


@plugin(category='factories', requires=['Rhino'])
def new_nurbssurface(*args, **kwargs):
    return super(NurbsSurface, RhinoNurbsSurface).__new__(RhinoNurbsSurface)


@plugin(category='factories', requires=['Rhino'])
def new_nurbssurface_from_parameters(*args, **kwargs):
    return RhinoNurbsSurface.from_parameters(*args, **kwargs)


@plugin(category='factories', requires=['Rhino'])
def new_nurbssurface_from_points(*args, **kwargs):
    return RhinoNurbsSurface.from_points(*args, **kwargs)


@plugin(category='factories', requires=['Rhino'])
def new_nurbssurface_from_fill(*args, **kwargs):
    return RhinoNurbsSurface.from_fill(*args, **kwargs)


@plugin(category='factories', requires=['Rhino'])
def new_nurbssurface_from_step(*args, **kwargs):
    return RhinoNurbsSurface.from_step(*args, **kwargs)


# src/compas_rhino/geometry/surfaces/nurbs.py

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from itertools import groupby

from compas.geometry import Point
from compas.geometry import NurbsSurface

from compas_rhino.conversions import point_to_rhino
from compas_rhino.conversions import point_to_compas
from compas_rhino.conversions import vector_to_compas
from compas_rhino.conversions import plane_to_compas_frame
from compas_rhino.conversions import box_to_compas
from compas_rhino.conversions import RhinoCurve

import Rhino.Geometry


def rhino_surface_from_parameters(points, weights, u_knots, v_knots, u_mults, v_mults, u_degree, v_degree, is_u_periodic=False, is_v_periodic=False):
    rhino_surface = Rhino.Geometry.NurbsSurface.Create(
        3,
        True,
        u_degree + 1,
        v_degree + 1,
        len(points[0]),
        len(points)
    )
    u_knotvector = [knot for knot, mult in zip(u_knots, u_mults) for _ in range(mult)]
    v_knotvector = [knot for knot, mult in zip(v_knots, v_mults) for _ in range(mult)]
    u_count = len(points[0])
    v_count = len(points)
    u_order = u_degree + 1
    v_order = v_degree + 1
    # account for superfluous knots
    # https://developer.rhino3d.com/guides/opennurbs/superfluous-knots/
    if len(u_knotvector) == u_count + u_order:
        u_knotvector[:] = u_knotvector[1:-1]
    if len(v_knotvector) == v_count + v_order:
        v_knotvector[:] = v_knotvector[1:-1]
    # add knots
    for index, knot in enumerate(u_knotvector):
        rhino_surface.KnotsU[index] = knot
    for index, knot in enumerate(v_knotvector):
        rhino_surface.KnotsV[index] = knot
    # add control points
    for i in range(v_count):
        for j in range(u_count):
            rhino_surface.Points.SetPoint(i, j, point_to_rhino(points[i][j]), weights[i][j])
    return rhino_surface


class RhinoNurbsSurface(NurbsSurface):
    """Class representing a NURBS surface.

    Attributes
    ----------
    points: list[list[:class:`compas.geometry.Point`]]
        The control points of the surface.
    weights: list[list[float]]
        The weights of the control points.
    u_knots: list[float]
        The knot vector, in the U direction, without duplicates.
    v_knots: list[float]
        The knot vector, in the V direction, without duplicates.
    u_mults: list[int]
        The multiplicities of the knots in the knot vector of the U direction.
    v_mults: list[int]
        The multiplicities of the knots in the knot vector of the V direction.
    u_degree: int
        The degree of the polynomials in the U direction.
    v_degree: int
        The degree of the polynomials in the V direction.
    u_domain: tuple[float, float]
        The parameter domain in the U direction.
    v_domain: tuple[float, float]
        The parameter domain in the V direction.
    is_u_periodic: bool
        True if the surface is periodic in the U direction.
    is_v_periodic: bool
        True if the surface is periodic in the V direction.

    """

    def __init__(self, name=None):
        super(RhinoNurbsSurface, self).__init__(name=name)
        self.rhino_surface = None

    # ==============================================================================
    # Data
    # ==============================================================================

    @property
    def data(self):
        # add superfluous knots
        # for compatibility with all/most other NURBS implementations
        # https://developer.rhino3d.com/guides/opennurbs/superfluous-knots/
        u_mults = self.u_mults[:]
        v_mults = self.v_mults[:]
        u_mults[0] += 1
        u_mults[-1] += 1
        v_mults[0] += 1
        v_mults[-1] += 1
        return {
            'points': [[point.data for point in row] for row in self.points],
            'weights': self.weights,
            'u_knots': self.u_knots,
            'v_knots': self.v_knots,
            'u_mults': u_mults,
            'v_mults': v_mults,
            'u_degree': self.u_degree,
            'v_degree': self.v_degree,
            'is_u_periodic': self.is_u_periodic,
            'is_v_periodic': self.is_v_periodic
        }

    @data.setter
    def data(self, data):
        points = [[Point.from_data(point) for point in row] for row in data['points']]
        weights = data['weights']
        u_knots = data['u_knots']
        v_knots = data['v_knots']
        u_mults = data['u_mults']
        v_mults = data['v_mults']
        u_degree = data['u_degree']
        v_degree = data['v_degree']
        is_u_periodic = data['is_u_periodic']
        is_v_periodic = data['is_v_periodic']
        self.rhino_surface = NurbsSurface.from_parameters(
            points, weights, u_knots, v_knots, u_mults, v_mults, u_degree, v_degree, is_u_periodic, is_v_periodic
        )

    @classmethod
    def from_data(cls, data):
        """Construct a BSpline surface from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`compas.geometry.NurbsSurface`
            The constructed surface.

        """
        points = [[Point.from_data(point) for point in row] for row in data['points']]
        weights = data['weights']
        u_knots = data['u_knots']
        v_knots = data['v_knots']
        u_mults = data['u_mults']
        v_mults = data['v_mults']
        u_degree = data['u_degree']
        v_degree = data['v_degree']
        is_u_periodic = data['is_u_periodic']
        is_v_periodic = data['is_v_periodic']
        return cls.from_parameters(
            points, weights, u_knots, v_knots, u_mults, v_mults, u_degree, v_degree, is_u_periodic, is_v_periodic
        )

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_rhino(cls, rhino_surface):
        """Construct a NURBS surface from an existing Rhino surface.

        Parameters
        ----------
        rhino_surface : :rhino:`Rhino.Geometry.NurbsSurface`
            A Rhino surface.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoNurbsSurface`

        """
        curve = cls()
        curve.rhino_surface = rhino_surface
        return curve

    @classmethod
    def from_parameters(cls, points, weights, u_knots, v_knots, u_mults, v_mults, u_degree, v_degree, is_u_periodic=False, is_v_periodic=False):
        """Construct a NURBS surface from explicit parameters.

        Parameters
        ----------
        points : list[list[:class:`compas.geometry.Point`]]
            The control points.
        weights : list[list[float]]
            The weights of the control points.
        u_knots : list[float]
            The knots in the U direction, without multiplicity.
        v_knots : list[float]
            The knots in the V direction, without multiplicity.
        u_mults : list[int]
            Multiplicity of the knots in the U direction.
        v_mults : list[int]
            Multiplicity of the knots in the V direction.
        u_degree : int
            Degree in the U direction.
        v_degree : int
            Degree in the V direction.

        Returns
        -------
        :class:`compas.geometry.NurbsSurface`

        """
        surface = cls()
        surface.rhino_surface = rhino_surface_from_parameters(points, weights, u_knots, v_knots, u_mults, v_mults, u_degree, v_degree)
        return surface

    @classmethod
    def from_points(cls, points, u_degree=3, v_degree=3):
        """Construct a NURBS surface from control points.

        Parameters
        ----------
        points : list[list[:class:`compas.geometry.Point`]]
            The control points.
        u_degree : int
            Degree in the U direction.
        v_degree : int
            Degree in the V direction.

        Returns
        -------
        :class:`compas.geometry.NurbsSurface`

        """
        points = list(zip(*points))
        u_count = len(points[0])
        v_count = len(points)
        points[:] = [point_to_rhino(point) for row in points for point in row]
        surface = cls()
        surface.rhino_surface = Rhino.Geometry.NurbsSurface.CreateFromPoints(points, v_count, u_count, u_degree, v_degree)
        return surface

    @classmethod
    def from_step(cls, filepath):
        """Load a NURBS surface from a STP file.

        Parameters
        ----------
        filepath : str

        Returns
        -------
        :class:`compas.geometry.NurbsSurface`

        """
        raise NotImplementedError

    @classmethod
    def from_fill(cls, curve1, curve2):
        """Construct a NURBS surface from the infill between two NURBS curves.

        Parameters
        ----------
        curve1 : :class:`compas.geometry.NurbsCurve`
        curve2 : :class:`compas.geometry.NurbsCurve`

        Returns
        -------
        :class:`compas.geometry.NurbsSurface`

        """
        surface = cls()
        # these curves probably need to be processed first
        surface.rhino_surface = Rhino.Geometry.NurbsSurface.CreateRuledSurface(curve1, curve2)
        return surface

    # ==============================================================================
    # Conversions
    # ==============================================================================

    def to_step(self, filepath, schema="AP203"):
        """Write the surface geometry to a STP file."""
        raise NotImplementedError

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def points(self):
        if self.rhino_surface:
            points = []
            for i in range(self.rhino_surface.Points.CountU):
                row = []
                for j in range(self.rhino_surface.Points.CountV):
                    row.append(point_to_compas(self.rhino_surface.Points.GetControlPoint(i, j).Location))
                points.append(row)
            return points

    @property
    def weights(self):
        if self.rhino_surface:
            weights = []
            for i in range(self.rhino_surface.Points.CountU):
                row = []
                for j in range(self.rhino_surface.Points.CountV):
                    row.append(self.rhino_surface.Points.GetWeight(i, j))
                weights.append(row)
            return weights

    @property
    def u_knots(self):
        if self.rhino_surface:
            return [key for key, _ in groupby(self.rhino_surface.KnotsU)]

    @property
    def u_knotsequence(self):
        if self.rhino_surface:
            return list(self.rhino_surface.KnotsU)

    @property
    def v_knots(self):
        if self.rhino_surface:
            return [key for key, _ in groupby(self.rhino_surface.KnotsV)]

    @property
    def v_knotsequence(self):
        if self.rhino_surface:
            return list(self.rhino_surface.KnotsV)

    @property
    def u_mults(self):
        if self.rhino_surface:
            return [len(list(group)) for _, group in groupby(self.rhino_surface.KnotsU)]

    @property
    def v_mults(self):
        if self.rhino_surface:
            return [len(list(group)) for _, group in groupby(self.rhino_surface.KnotsV)]

    @property
    def u_degree(self):
        if self.rhino_surface:
            return self.rhino_surface.Degree(0)

    @property
    def v_degree(self):
        if self.rhino_surface:
            return self.rhino_surface.Degree(1)

    @property
    def u_domain(self):
        if self.rhino_surface:
            return self.rhino_surface.Domain(0)

    @property
    def v_domain(self):
        if self.rhino_surface:
            return self.rhino_surface.Domain(1)

    @property
    def is_u_periodic(self):
        if self.rhino_surface:
            return self.rhino_surface.IsPeriodic(0)

    @property
    def is_v_periodic(self):
        if self.rhino_surface:
            return self.rhino_surface.IsPeriodic(1)

    # ==============================================================================
    # Methods
    # ==============================================================================

    def u_isocurve(self, u):
        """Compute the isoparametric curve at parameter u.

        Parameters
        ----------
        u : float

        Returns
        -------
        :class:`compas.geometry.NurbsCurve`

        """
        curve = self.rhino_surface.IsoCurve(1, u)
        return RhinoCurve.from_geometry(curve).to_compas()

    def v_isocurve(self, v):
        """Compute the isoparametric curve at parameter v.

        Parameters
        ----------
        v : float

        Returns
        -------
        :class:`compas.geometry.NurbsCurve`

        """
        curve = self.rhino_surface.IsoCurve(0, v)
        return RhinoCurve.from_geometry(curve).to_compas()

    def boundary(self):
        """Compute the boundary curves of the surface.

        Returns
        -------
        list[:class:`compas.geometry.NurbsCurve`]

        """
        raise NotImplementedError

    def point_at(self, u, v):
        """Compute a point on the surface.

        Parameters
        ----------
        u : float
        v : float

        Returns
        -------
        :class:`compas.geometry.Point`

        """
        point = self.rhino_surface.PointAt(u, v)
        return point_to_compas(point)

    def curvature_at(self, u, v):
        """Compute the curvature at a point on the surface.

        Parameters
        ----------
        u : float
        v : float

        Returns
        -------
        :class:`compas.geometry.Vector`

        """
        vector = self.rhino_surface.CurvatureAt(u, v)
        return vector_to_compas(vector)

    def frame_at(self, u, v):
        """Compute the local frame at a point on the curve.

        Parameters
        ----------
        u : float
        v : float

        Returns
        -------
        :class:`compas.geometry.Frame`

        """
        result, plane = self.rhino_surface.FrameAt(u, v)
        if result:
            return plane_to_compas_frame(plane)

    def closest_point(self, point, return_parameters=False):
        """Compute the closest point on the curve to a given point.

        Parameters
        ----------
        point : :class:`compas.geometry.Point`
            The test point.
        return_parameters : bool, optional
            If True, return the UV parameters of the closest point as tuple in addition to the point location.

        Returns
        -------
        :class:`compas.geometry.Point`
            If `return_parameters` is False.
        :class:`compas.geometry.Point`, (float, float)
            If `return_parameters` is True.

        """
        result, u, v = self.rhino_surface.ClosestPoint(point_to_rhino(point))
        if not result:
            return
        point = self.point_at(u, v)
        if return_parameters:
            return point, (u, v)
        return point

    def aabb(self, precision=0.0, optimal=False):
        """Compute the axis aligned bounding box of the surface.

        Parameters
        ----------
        precision : float, optional
        optimal : float, optional
            Flag indicating that the box should be precise.

        Returns
        -------
        :class:`compas.geometry.Box`

        """
        box = self.rhino_surface.GetBoundingBox(optimal)
        return box_to_compas(box)


# src/compas_rhino/geometry/surfaces/surface.py
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.geometry import Surface

from compas_rhino.conversions import point_to_rhino
from compas_rhino.conversions import point_to_compas
from compas_rhino.conversions import vector_to_compas
from compas_rhino.conversions import plane_to_compas_frame
from compas_rhino.conversions import box_to_compas

from compas_rhino.geometry.curves import RhinoCurve


class RhinoSurface(Surface):
    """Class representing a general surface object.

    Attributes
    ----------
    u_degree: int
        The degree of the polynomials in the U direction.
    v_degree: int
        The degree of the polynomials in the V direction.
    u_domain: tuple[float, float]
        The parameter domain in the U direction.
    v_domain: tuple[float, float]
        The parameter domain in the V direction.
    is_u_periodic: bool
        True if the surface is periodic in the U direction.
    is_v_periodic: bool
        True if the surface is periodic in the V direction.

    """

    def __init__(self, name=None):
        super(RhinoSurface, self).__init__(name=name)
        self._rhino_surface = None

    @property
    def rhino_surface(self):
        return self._rhino_surface

    @rhino_surface.setter
    def rhino_surface(self, surface):
        self._rhino_surface = surface

    # ==============================================================================
    # Data
    # ==============================================================================

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_rhino(cls, rhino_surface):
        """Construct a NURBS surface from an existing Rhino surface.

        Parameters
        ----------
        rhino_surface : :rhino:`Rhino.Geometry.Surface`
            A Rhino surface.

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoSurface`

        """
        curve = cls()
        curve.rhino_surface = rhino_surface
        return curve

    # ==============================================================================
    # Conversions
    # ==============================================================================

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def u_degree(self):
        if self.rhino_surface:
            return self.rhino_surface.Degree(0)

    @property
    def v_degree(self):
        if self.rhino_surface:
            return self.rhino_surface.Degree(1)

    @property
    def u_domain(self):
        if self.rhino_surface:
            return self.rhino_surface.Domain(0)

    @property
    def v_domain(self):
        if self.rhino_surface:
            return self.rhino_surface.Domain(1)

    @property
    def is_u_periodic(self):
        if self.rhino_surface:
            return self.rhino_surface.IsPeriodic(0)

    @property
    def is_v_periodic(self):
        if self.rhino_surface:
            return self.rhino_surface.IsPeriodic(1)

    # ==============================================================================
    # Methods
    # ==============================================================================

    def u_isocurve(self, u):
        """Compute the isoparametric curve at parameter u.

        Parameters
        ----------
        u : float

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoCurve`

        """
        curve = self.rhino_surface.IsoCurve(1, u)
        return RhinoCurve.from_rhino(curve)

    def v_isocurve(self, v):
        """Compute the isoparametric curve at parameter v.

        Parameters
        ----------
        v : float

        Returns
        -------
        :class:`compas_rhino.geometry.RhinoCurve`

        """
        curve = self.rhino_surface.IsoCurve(0, v)
        return RhinoCurve.from_rhino(curve)

    def boundary(self):
        """Compute the boundary curves of the surface.

        Returns
        -------
        list[:class:`compas_rhino.geometry.RhinoCurve`]

        """
        raise NotImplementedError

    def point_at(self, u, v):
        """Compute a point on the surface.

        Parameters
        ----------
        u : float
        v : float

        Returns
        -------
        :class:`compas.geometry.Point`

        """
        point = self.rhino_surface.PointAt(u, v)
        return point_to_compas(point)

    def curvature_at(self, u, v):
        """Compute the curvature at a point on the surface.

        Parameters
        ----------
        u : float
        v : float

        Returns
        -------
        :class:`compas.geometry.Vector`

        """
        vector = self.rhino_surface.CurvatureAt(u, v)
        return vector_to_compas(vector)

    def frame_at(self, u, v):
        """Compute the local frame at a point on the curve.

        Parameters
        ----------
        u : float
        v : float

        Returns
        -------
        :class:`compas.geometry.Frame`

        """
        result, plane = self.rhino_surface.FrameAt(u, v)
        if result:
            return plane_to_compas_frame(plane)

    def closest_point(self, point, return_parameters=False):
        """Compute the closest point on the curve to a given point.

        Parameters
        ----------
        point : :class:`compas.geometry.Point`
            The test point.
        return_parameters : bool, optional
            If True, return the UV parameters of the closest point as tuple in addition to the point location.

        Returns
        -------
        :class:`compas.geometry.Point`
            If `return_parameters` is False.
        :class:`compas.geometry.Point`, (float, float)
            If `return_parameters` is True.

        """
        result, u, v = self.rhino_surface.ClosestPoint(point_to_rhino(point))
        if not result:
            return
        point = self.point_at(u, v)
        if return_parameters:
            return point, (u, v)
        return point

    def aabb(self, precision=0.0, optimal=False):
        """Compute the axis aligned bounding box of the surface.

        Parameters
        ----------
        precision : float, optional
        optimal : float, optional
            Flag indicating that the box should be precise.

        Returns
        -------
        :class:`compas.geometry.Box`

        """
        box = self.rhino_surface.GetBoundingBox(optimal)
        return box_to_compas(box)
