ct to an already existing service

    Examples
    --------
    Minimal example showing connection to the proxy server, and ensuring the
    server is disposed after using it:

    >>> from compas.rpc import Proxy                        # doctest: +SKIP
    >>> with Proxy('compas.numerical') as numerical:        # doctest: +SKIP
    ...     pass                                            # doctest: +SKIP
    ...                                                     # doctest: +SKIP
    Starting a new proxy server...                          # doctest: +SKIP
    New proxy server started.                               # doctest: +SKIP
    Stopping the server proxy.                              # doctest: +SKIP
    """

    def __init__(
        self,
        package=None,
        python=None,
        url="http://127.0.0.1",
        port=1753,
        service=None,
        max_conn_attempts=100,
        autoreload=True,
        capture_output=True,
        path=None,
    ):
        self._package = None
        self._python = compas._os.select_python(python)
        self._url = url
        self._port = port
        self.max_conn_attempts = max_conn_attempts
        self._service = None
        self._process = None
        self._function = None
        self._profile = None
        self._path = path

        self.service = service
        self.package = package
        self.autoreload = autoreload
        self.capture_output = capture_output

        self._implicitely_started_server = False
        self._server = self._try_reconnect()
        if self._server is None:
            self._server = self.start_server()
            self._implicitely_started_server = True

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def address(self):
        return "{}:{}".format(self._url, self._port)

    @property
    def profile(self):
        return self._profile

    @profile.setter
    def profile(self, profile):
        self._profile = profile

    @property
    def package(self):
        return self._package

    @package.setter
    def package(self, package):
        self._package = package

    @property
    def service(self):
        return self._service

    @service.setter
    def service(self, service):
        if not service:
            self._service = "compas.rpc.services.default"
        else:
            self._service = service

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

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

    # ==========================================================================
    # customization
    # ==========================================================================

    def __enter__(self):
        return self

    def __exit__(self, *args):
        # If we started the RPC server, we will try to clean up and stop it
        # otherwise we just disconnect from it
        if self._implicitely_started_server:
            self.stop_server()
        else:
            self._server.__close()

    def __getattr__(self, name):
        """Find server attributes (methods) corresponding to attributes that do not exist on the proxy itself.

        1. Use :attr:`package` as a namespace for the requested attribute to create a fully qualified path on the server.
        2. Try to get the fully qualified attribute from :attr:`_server`.
        3. If successful, store the result in :attr:`_function`.
        3. Return a handle to :meth:`_proxy`, which will delegate calls to :attr:`_function`.

        Returns
        -------
        callable

        """
        if self.package:
            name = "{}.{}".format(self.package, name)
        try:
            self._function = getattr(self._server, name)
        except Exception:
            raise RPCServerError()
        return self._proxy

    # ==========================================================================
    # methods
    # ==========================================================================

    def _try_reconnect(self):
        """Try and reconnect to an existing proxy server.

        Returns
        -------
        ServerProxy
            Instance of the proxy if reconnection succeeded, otherwise ``None``.

        """
        server = ServerProxy(self.address)
        try:
            server.ping()
        except Exception:
            return None
        else:
            print("Reconnecting to an existing server proxy.")
        return server

    def start_server(self):
        """Start the remote server.

        Returns
        -------
        ServerProxy
            Instance of the proxy, if the connection was successful.

        Raises
        ------
        RPCServerError
            If the server providing the requested service cannot be reached after
            100 contact attempts (*pings*). The number of attempts is set by
            :attr:`Proxy.max_conn_attempts`.

        Examples
        --------
        >>> p = Proxy()  # doctest: +SKIP
        >>> p.stop_server()  # doctest: +SKIP
        >>> p.start_server()  # doctest: +SKIP

        """
        env = compas._os.prepare_environment()
        # this part starts the server side of the RPC setup
        # it basically launches a subprocess
        # to start the default service
        # the default service creates a server
        # and registers a dispatcher for custom functionality
        try:
            Popen
        except NameError:
            self._process = Process()
            for name in env:
                if self._process.StartInfo.EnvironmentVariables.ContainsKey(name):
                    self._process.StartInfo.EnvironmentVariables[name] = env[name]
                else:
                    self._process.StartInfo.EnvironmentVariables.Add(name, env[name])
            self._process.StartInfo.UseShellExecute = False
            self._process.StartInfo.RedirectStandardOutput = self.capture_output
            self._process.StartInfo.RedirectStandardError = self.capture_output
            self._process.StartInfo.FileName = self.python
            self._process.StartInfo.Arguments = "-m {0} --port {1} --{2}autoreload".format(
                self.service, self._port, "" if self.autoreload else "no-"
            )
            self._process.Start()
        else:
            args = [
                self.python,
                "-m",
                self.service,
                "--port",
                str(self._port),
                "--{}autoreload".format("" if self.autoreload else "no-"),
            ]
            kwargs = dict(env=env)
            if self.capture_output:
                kwargs["stdout"] = PIPE
                kwargs["stderr"] = PIPE

            self._process = Popen(args, **kwargs)
        # this starts the client side
        # it creates a proxy for the server
        # and tries to connect the proxy to the actual server
        server = ServerProxy(self.address)
        print("Starting a new proxy server...")
        success = False
        attempt_count = 0
        while attempt_count < self.max_conn_attempts:
            try:
                server.ping()
            except Exception:
                time.sleep(0.1)
                attempt_count += 1
                print("    {} attempts left.".format(self.max_conn_attempts - attempt_count))
            else:
                success = True
                break
        if not success:
            raise RPCServerError("The server is not available.")
        else:
            print("New proxy server started.")
        return server

    def stop_server(self):
        """Stop the remote server and terminate/kill the python process that was used to start it.

        Returns
        -------
        None

        Examples
        --------
        >>> p = Proxy()  # doctest: +SKIP
        >>> p.stop_server()  # doctest: +SKIP
        >>> p.start_server()  # doctest: +SKIP

        """
        print("Stopping the server proxy.")
        try:
            self._server.remote_shutdown()
        except Exception:
            pass
        self._terminate_process()

    def restart_server(self):
        """Restart the server.

        Returns
        -------
        None

        """
        self.stop_server()
        self.start_server()

    def _terminate_process(self):
        """Attempts to terminate the python process hosting the proxy server.

        The process reference might not be present, e.g. in the case
        of reusing an existing connection. In that case, this is a no-op.
        """
        if not self._process:
            return
        try:
            self._process.terminate()
        except Exception:
            pass
        try:
            self._process.kill()
        except Exception:
            pass

    def _proxy(self, *args, **kwargs):
        """Callable replacement for the requested functionality.

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

        Returns
        -------
        object
            The 'data' part of the result dict returned by the remote function.
            The result dict has the following structure ::

                {
                    'error'   : ...,  # A traceback of the error raised on the server, if any.
                    'profile' : ...,  # A profile of the code executed on the server, if there was no error.
                    'data'    : ...,  # The result returned by the target function, if there was no error.
                }

        Warnings
        --------
        The `args` and `kwargs` have to be JSON-serializable.
        This means that, currently, only COMPAS data objects (geometry, robots, data structures) and native Python objects are supported.
        The returned results will also always be in the form of COMPAS data objects and built-in Python objects.
        Numpy objects are automatically converted to their built-in Python equivalents.

        """
        idict = {"args": args, "kwargs": kwargs}
        istring = json.dumps(idict, cls=DataEncoder)
        # it makes sense that there is a broken pipe error
        # because the process is not the one receiving the feedback
        # when there is a print statement on the server side
        # this counts as output
        # it should be sent as part of RPC communication
        try:
            ostring = self._function(istring, self._path or "")
        except Exception:
            # not clear what the point of this is
            # self.stop_server()
            # if this goes wrong, it means a Fault error was generated by the server
            # no need to stop the server for this
            raise

        if not ostring:
            raise RPCServerError("No output was generated.")

        result = json.loads(ostring, cls=DataDecoder)

        if result["error"]:
            raise RPCServerError(result["error"])

        self.profile = result["profile"]
        return result["data"]


# src/compas/rpc/server.py

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

import threading

try:
    from SimpleXMLRPCServer import SimpleXMLRPCServer
except ImportError:
    from xmlrpc.server import SimpleXMLRPCServer


class Server(SimpleXMLRPCServer):
    """Version of a `SimpleXMLRPCServer` that can be cleanly terminated from the client side.

    Notes
    -----
    This class has to be used by a service to start the XMLRPC server in a way
    that can be pinged to check if the server is alive, and can be cleanly terminated.

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

        # service.py

        from compas.rpc import Server
        from compas.rpc import Dispatcher


        class DefaultService(Dispatcher):
            pass


        if __name__ == '__main__':

            server = Server(("localhost", 8888))

            server.register_function(server.ping)
            server.register_function(server.remote_shutdown)
            server.register_instance(DefaultService())
            server.serve_forever()

    """

    def ping(self):
        """Simple function used to check if a remote server can be reached.

        Returns
        -------
        int
            Always returns 1, since the ping is considered a success
            if the function can be reached from the client.

        Notes
        -----
        Should be used together with an instance of `compas.rpc.Server`.

        """
        return 1

    def remote_shutdown(self):
        """Stop the server through a call from the client side.

        Returns
        -------
        int
            Always returns 1.

        """
        threading.Thread(target=self._shutdown_thread).start()
        return 1

    def _shutdown_thread(self):
        self.shutdown()


# src/compas/rpc/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.data import DataEncoder
from compas.data 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()


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.data import DataEncoder
from compas.data 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

        vertex_index = mesh.vertex_index()
        vertices     = mesh.vertices_attributes('xyz')
        edges        = [(vertex_index[u], vertex_index[v]) for u, v in mesh.edges()]
        fixed        = [vertex_index[vertex] for vertex 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 vertex, attr in mesh.vertices(True):
            attr['x'] = xyz[vertex][0]
            attr['y'] = xyz[vertex][1]
            attr['z'] = xyz[vertex][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/topology/__init__.py

from __future__ import absolute_import

import compas

from .traversal import (
    depth_first_ordering,
    breadth_first_ordering,
    breadth_first_traverse,
    breadth_first_paths,
    shortest_path,
    astar_lightest_path,
    astar_shortest_path,
    dijkstra_distances,
    dijkstra_path,
)
from .combinatorics import vertex_coloring, connected_components
from .orientation import face_adjacency, unify_cycles
from .connectivity import adjacency_from_edges

if compas.RHINO:
    from .orientation_rhino import face_adjacency_rhino, unify_cycles_rhino

if not compas.IPY:
    from .orientation_numpy import face_adjacency_numpy, unify_cycles_numpy


__all__ = [
    "depth_first_ordering",
    "breadth_first_ordering",
    "breadth_first_traverse",
    "breadth_first_paths",
    "shortest_path",
    "astar_lightest_path",
    "astar_shortest_path",
    "dijkstra_distances",
    "dijkstra_path",
    "vertex_coloring",
    "connected_components",
    "face_adjacency",
    "unify_cycles",
    "adjacency_from_edges",
]

if compas.RHINO:
    __all__ += [
        "face_adjacency_rhino",
        "unify_cycles_rhino",
    ]

if not compas.IPY:
    __all__ += [
        "face_adjacency_numpy",
        "unify_cycles_numpy",
    ]


# src/compas/topology/combinatorics.py

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

from collections import deque
from compas.topology.traversal import breadth_first_traverse


def vertex_coloring(adjacency):
    """Color the vertices of a graph such that no two colors are adjacent.

    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
    -------
    dict[hashable, int]
        A dict mapping each node of the graph to a color number.

    Notes
    -----
    This algorithms works on any data structure that can be interpreted as a graph, e.g.
    networks, meshes, volmeshes, etc..

    For more info, see [1]_.

    References
    ----------
    .. [1] Chu-Carroll, M. *Graph Coloring Algorithms*.
           Available at: http://scienceblogs.com/goodmath/2007/06/28/graph-coloring-algorithms-1/.

    Warnings
    --------
    This is a greedy algorithm, so it might be slow for large networks.

    Examples
    --------
    >>> import compas
    >>> from compas.datastructures import Network
    >>> network = Network.from_obj(compas.get('lines.obj'))
    >>> key_color = vertex_coloring(network.adjacency)
    >>> key = network.get_any_node()
    >>> color = key_color[key]
    >>> any(key_color[nbr] == color for nbr in network.neighbors(key))
    False

    """
    key_to_color = {}
    key_to_degree = {key: len(adjacency[key]) for key in adjacency}
    vertices = sorted(adjacency.keys(), key=lambda key: key_to_degree[key])
    uncolored = deque(vertices[::-1])
    current_color = 0
    while uncolored:
        a = uncolored.popleft()
        key_to_color[a] = current_color
        colored_with_current = [a]
        for b in uncolored:
            if not any(b in adjacency[key] for key in colored_with_current):
                key_to_color[b] = current_color
                colored_with_current.append(b)
        for key in colored_with_current[1:]:
            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.traversal 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.traversal 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.traversal 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 1e17) 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

from __future__ import absolute_import

from .azync import await_callback
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 .images import gif_from_images
from .itertools import (
    flatten,
    reshape,
    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

__all__ = [
    "await_callback",
    "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",
    "gif_from_images",
    "normalize_values",
    "remap_values",
    "meshgrid",
    "linspace",
    "reshape",
    "flatten",
    "pairwise",
    "window",
    "iterable_like",
    "grouper",
    "geometric_key",
    "reverse_geometric_key",
    "geometric_key_xy",
    "download_file_from_remote",
    "SSH",
]


# src/compas/utilities/azync.py

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

import sys
import threading


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/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.0, g / 255.0, b / 255.0


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 | [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


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


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/images.py

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

import os
import compas

if not compas.IPY:
    import imageio


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
from functools import reduce
from operator import mul

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


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.

    Raises
    ------
    ValueError
        If the number of elements is less than 2.

    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]

    """
    if num < 2:
        raise ValueError("ValueError: number of elements must be at least 2, got {:d}".format(num))

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


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 reshape(lst, shape):
    """Gives a new shape to an array without changing its data.

    This function mimics the functionality of ``numpy.reshape`` [1]_, but in a simpler form.

    Parameters
    ----------
    lst : list
        A list of items.
    shape : int or tuple of ints
        The new shape of the list


    Examples
    --------
    >>> a = [1, 2, 3, 4, 5, 6]
    >>> reshape(a, (2, 3))
    [[1, 2, 3], [4, 5, 6]]
    >>> reshape(a, (3, 2))
    [[1, 2], [3, 4], [5, 6]]
    >>> a = [[1, 2], [3, 4], [5, 6]]
    >>> reshape(a, (2, 3))
    [[1, 2, 3], [4, 5, 6]]


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

    """

    def helper(l, shape):  # noqa E741
        if len(shape) == 1:
            if len(l) % shape[0] != 0:
                raise ValueError("ValueError: cannot reshape array of size %d into shape %s" % (len(lst), shape))
            return l
        else:
            n = reduce(mul, shape[1:])
            return [helper(l[i * n : (i + 1) * n], shape[1:]) for i in range(len(l) // n)]

    shape = (shape,) if isinstance(shape, int) else shape
    flattened_list = list(flatten(lst)) if isinstance(lst[0], list) else lst
    if len(list(flattened_list)) != reduce(lambda x, y: x * y, shape):
        raise ValueError("ValueError: cannot reshape array of size %d into shape %s" % (len(lst), shape))
    return helper(flattened_list, shape)


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


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


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


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/geometry/_core/_algebra.py

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

from math import sqrt
from math import fabs


def vector_average(vector):
    """Average of a vector.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        List of values.

    Returns
    -------
    float
        The mean value.
    """
    return sum(vector) / float(len(vector))


def vector_variance(vector):
    """Variance of a vector.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        List of values.

    Returns
    -------
    float
        The variance value.
    """
    m = vector_average(vector)
    return (sum([(i - m) ** 2 for i in vector]) / float(len(vector))) ** 0.5


def vector_standard_deviation(vector):
    """Standard deviation of a vector.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        List of values.

    Returns
    -------
    float
        The standard deviation value.
    """
    return vector_variance(vector) ** 0.5


def close(value1, value2, tol=1e-05):
    """Returns True if two values are equal within a tolerance.

    Parameters
    ----------
    value1 : float or int
    value2 : float or int
    tol : float, optional
        The tolerance for comparing values.

    Returns
    -------
    bool
        True if the values are closer than the tolerance.
        False otherwise.

    Examples
    --------
    >>> close(1., 1.001)
    False
    >>> close(1., 1.001, tol=1e-2)
    True
    """
    return fabs(value1 - value2) < tol


def allclose(l1, l2, tol=1e-05):
    """Returns True if two lists are element-wise equal within a tolerance.

    Parameters
    ----------
    l1 : sequence[float]
        The first list of values.
    l2 : sequence[float]
        The second list of values.
    tol : float, optional
        The tolerance for comparing values.

    Returns
    -------
    bool
        True if all corresponding values of the two lists are closer than the tolerance.
        False otherwise.

    Notes
    -----
    The function is similar to NumPy's *allclose* function [1]_.

    References
    ----------
    .. [1] https://docs.scipy.org/doc/numpy/reference/generated/numpy.allclose.html

    Examples
    --------
    >>> allclose([0.1, 0.2, 0.3, 0.4], [0.1, 0.20001, 0.3, 0.4])
    True

    >>> allclose([0.1, 0.2, 0.3, 0.4], [0.1, 0.20001, 0.3, 0.4], tol=1e-6)
    False

    """

    if any(not allclose(a, b, tol) if hasattr(a, "__iter__") else fabs(a - b) > tol for a, b in zip(l1, l2)):
        return False
    return True


def argmax(values):
    """Returns the index of the first maximum value within an array.

    Parameters
    ----------
    values : sequence[float]
        A list of values.

    Returns
    -------
    int
        The index of the first maximum value within an array.

    Notes
    -----
    NumPy's *argmax* function [1]_ is different, it returns an array of indices.

    References
    ----------
    .. [1] https://numpy.org/doc/stable/reference/generated/numpy.argmax.html

    Examples
    --------
    >>> argmax([2, 4, 4, 3])
    1

    """
    return max(range(len(values)), key=lambda i: values[i])  # type: ignore


def argmin(values):
    """Returns the index of the first minimum value within an array.

    Parameters
    ----------
    values : sequence[float]
        A list of values.

    Returns
    -------
    int
        The index of the first minimum value within an array.

    Notes
    -----
    NumPy's *argmin* function [1]_ is different, it returns an array of indices.

    References
    ----------
    .. [1] https://numpy.org/doc/stable/reference/generated/numpy.argmin.html

    Examples
    --------
    >>> argmin([4, 2, 2, 3])
    1

    """
    return min(range(len(values)), key=lambda i: values[i])


# ==============================================================================
# these return something of smaller dimension/length/...
# something_(of)vector/s
# ==============================================================================


def sum_vectors(vectors, axis=0):
    """Calculate the sum of a series of vectors along the specified axis.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.
    axis : int, optional
        If ``axis == 0``, the sum is taken per column.
        If ``axis == 1``, the sum is taken per row.

    Returns
    -------
    list[float]
        The length of the list is ``len(vectors[0])``, if ``axis == 0``.
        The length is ``len(vectors)``, otherwise.

    Examples
    --------
    >>> vectors = [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]
    >>> sum_vectors(vectors)
    [3.0, 6.0, 9.0]
    >>> sum_vectors(vectors, axis=1)
    [6.0, 6.0, 6.0]

    """
    if axis == 0:
        vectors = zip(*vectors)
    return [sum(vector) for vector in vectors]


def norm_vector(vector):
    """Calculate the length of a vector.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.

    Returns
    -------
    float
        The L2 norm, or *length* of the vector.

    Examples
    --------
    >>> norm_vector([2.0, 0.0, 0.0])
    2.0

    >>> norm_vector([1.0, 1.0, 0.0]) == sqrt(2.0)
    True

    """
    return sqrt(sum(axis**2 for axis in vector))


def norm_vectors(vectors):
    """
    Calculate the norm of each vector in a list of vectors.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors

    Returns
    -------
    list[float]
        A list with the lengths of all vectors.

    Examples
    --------
    >>> norm_vectors([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]])
    [1.0, 2.0, 3.0]

    """
    return [norm_vector(vector) for vector in vectors]


def length_vector(vector):
    """Calculate the length of the vector.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.

    Returns
    -------
    float
        The length of the vector.

    Examples
    --------
    >>> length_vector([2.0, 0.0, 0.0])
    2.0

    >>> length_vector([1.0, 1.0, 0.0]) == sqrt(2.0)
    True

    """
    return sqrt(length_vector_sqrd(vector))


def length_vector_xy(vector):
    """Compute the length of a vector, assuming it lies in the XY plane.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) components of the vector.

    Returns
    -------
    float
        The length of the XY component of the vector.

    Examples
    --------
    >>> length_vector_xy([2.0, 0.0])
    2.0

    >>> length_vector_xy([2.0, 0.0, 0.0])
    2.0

    >>> length_vector_xy([2.0, 0.0, 2.0])
    2.0

    """
    return sqrt(length_vector_sqrd_xy(vector))


def length_vector_sqrd(vector):
    """Compute the squared length of a vector.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.

    Returns
    -------
    float
        The squared length.

    Examples
    --------
    >>> length_vector_sqrd([1.0, 1.0, 0.0])
    2.0

    """
    return vector[0] ** 2 + vector[1] ** 2 + vector[2] ** 2


def length_vector_sqrd_xy(vector):
    """Compute the squared length of a vector, assuming it lies in the XY plane.

    Parameters
    ----------
    vector : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) components of the vector.

    Returns
    -------
    float
        The squared length.

    Examples
    --------
    >>> length_vector_sqrd_xy([1.0, 1.0])
    2.0

    >>> length_vector_sqrd_xy([1.0, 1.0, 0.0])
    2.0

    >>> length_vector_sqrd_xy([1.0, 1.0, 1.0])
    2.0

    """
    return vector[0] ** 2 + vector[1] ** 2


# ==============================================================================
# these perform an operation on a vector and return a modified vector
# -> elementwise operations on 1 vector
# should this not bet ...ed_vector
# ... or else modify the vector in-place
# ==============================================================================


def scale_vector(vector, factor):
    """Scale a vector by a given factor.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.
    factor : float
        The scaling factor.

    Returns
    -------
    [float, float, float]
        The scaled vector.

    Examples
    --------
    >>> scale_vector([1.0, 2.0, 3.0], 2.0)
    [2.0, 4.0, 6.0]

    >>> v = [2.0, 0.0, 0.0]
    >>> scale_vector(v, 1 / length_vector(v))
    [1.0, 0.0, 0.0]

    """
    return [axis * factor for axis in vector]


def scale_vector_xy(vector, factor):
    """Scale a vector by a given factor, assuming it lies in the XY plane.

    Parameters
    ----------
    vector : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) components of the vector.
    scale : float
        Scale factor.

    Returns
    -------
    [float, float, 0.0]
        The scaled vector in the XY-plane.

    Examples
    --------
    >>> scale_vector_xy([1.0, 2.0, 3.0], 2.0)
    [2.0, 4.0, 0.0]

    """
    return [vector[0] * factor, vector[1] * factor, 0.0]


def scale_vectors(vectors, factor):
    """Scale multiple vectors by a given factor.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.
    factor : float
        The scaling factor.

    Returns
    -------
    list[[float, float, float]]
        The scaled vectors.

    Examples
    --------
    >>>

    """
    return [scale_vector(vector, factor) for vector in vectors]


def scale_vectors_xy(vectors, factor):
    """Scale multiple vectors by a given factor, assuming they lie in the XY plane.

    Parameters
    ----------
    vectors : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.
    factor : float
        The scaling factor.

    Returns
    -------
    list[[float, float, 0.0]]
        The scaled vectors in the XY plane.

    Examples
    --------
    >>>

    """
    return [scale_vector_xy(vector, factor) for vector in vectors]


def normalize_vector(vector):
    """Normalise a given vector.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.

    Returns
    -------
    [float, float, float]
        The normalized vector.

    Examples
    --------
    >>>

    """
    length = length_vector(vector)
    if not length:
        return vector
    return [vector[0] / length, vector[1] / length, vector[2] / length]


def normalize_vector_xy(vector):
    """Normalize a vector, assuming it lies in the XY-plane.

    Parameters
    ----------
    vector : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) components of the vector.

    Returns
    -------
    [float, float, 0.0]
        The normalized vector in the XY-plane.

    Examples
    --------
    >>>

    """
    length = length_vector_xy(vector)
    if not length:
        return vector
    return [vector[0] / length, vector[1] / length, 0.0]


def normalize_vectors(vectors):
    """Normalise multiple vectors.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.

    Returns
    -------
    list[[float, float, float]]
        The normalized vectors.

    Examples
    --------
    >>>

    """
    return [normalize_vector(vector) for vector in vectors]


def normalize_vectors_xy(vectors):
    """Normalise multiple vectors, assuming they lie in the XY plane.

    Parameters
    ----------
    vectors : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.

    Returns
    -------
    list[[float, float, 0.0]]
        The normalized vectors in the XY plane.

    Examples
    --------
    >>>

    """
    return [normalize_vector_xy(vector) for vector in vectors]


def power_vector(vector, power):
    """Raise a vector to the given power.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.
    power : int, float
        The power to which to raise the vector.

    Returns
    -------
    [float, float, float]
        The raised vector.

    Examples
    --------
    >>>

    """
    return [axis**power for axis in vector]


def power_vectors(vectors, power):
    """Raise a list of vectors to the given power.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.
    power : int, float
        The power to which to raise the vectors.

    Returns
    -------
    list[[float, float, float]]
        The raised vectors.

    Examples
    --------
    >>>

    """
    return [power_vector(vector, power) for vector in vectors]


def square_vector(vector):
    """Raise a vector to the power 2.

    Parameters
    ----------
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.

    Returns
    -------
    [float, float, float]
        The squared vector.

    Examples
    --------
    >>>

    """
    return power_vector(vector, 2)


def square_vectors(vectors):
    """Raise a multiple vectors to the power 2.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.

    Returns
    -------
    [float, float, float]]
        The squared vectors.

    Examples
    --------
    >>>

    """
    return [square_vectors(vector) for vector in vectors]


# ==============================================================================
# these perform an operation with corresponding elements of the (2) input vectors as operands
# and return a vector with the results
# -> elementwise operations on two vectors
# ==============================================================================


def add_vectors(u, v):
    """Add two vectors.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the second vector.

    Returns
    -------
    [float, float, float]
        The resulting vector.

    """
    return [a + b for (a, b) in zip(u, v)]


def add_vectors_xy(u, v):
    """Add two vectors, assuming they lie in the XY-plane.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) components of the first vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) components of the second vector.

    Returns
    -------
    [float, float, 0.0]
        Resulting vector in the XY-plane.

    Examples
    --------
    >>>

    """
    return [u[0] + v[0], u[1] + v[1], 0.0]


def subtract_vectors(u, v):
    """Subtract one vector from another.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the second vector.

    Returns
    -------
    [float, float, float]
        The resulting vector.

    Examples
    --------
    >>>

    """
    return [a - b for (a, b) in zip(u, v)]


def subtract_vectors_xy(u, v):
    """Subtract one vector from another, assuming they lie in the XY plane.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The XY(Z) components of the first vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The XY(Z) components of the second vector.

    Returns
    -------
    [float, float, 0.0]
        Resulting vector in the XY-plane.

    Examples
    --------
    >>>

    """
    return [u[0] - v[0], u[1] - v[1], 0.0]


def multiply_vectors(u, v):
    """Element-wise multiplication of two vectors.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        The XYZ components of the first vector.
    v : l[float, float, float] | :class:`~compas.geometry.Vector`
        The XYZ components of the second vector.

    Returns
    -------
    [float, float, float]
        Resulting vector.

    Examples
    --------
    >>>

    """
    return [a * b for (a, b) in zip(u, v)]


def multiply_vectors_xy(u, v):
    """Element-wise multiplication of two vectors assumed to lie in the XY plane.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The XY(Z) components of the first vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The XY(Z) components of the second vector.

    Returns
    -------
    [float, float, 0.0]
        Resulting vector in the XY plane.

    Examples
    --------
    >>>

    """
    return [u[0] * v[0], u[1] * v[1], 0.0]


def divide_vectors(u, v):
    """Element-wise division of two vectors.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        The XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        The XYZ components of the second vector.

    Returns
    -------
    [float, float, float]
        Resulting vector.

    Examples
    --------
    >>>

    """
    return [a / b for (a, b) in zip(u, v)]


def divide_vectors_xy(u, v):
    """Element-wise division of two vectors assumed to lie in the XY plane.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The XY(Z) components of the first vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The XY(Z) components of the second vector.

    Returns
    -------
    [float, float, 0.0]
        Resulting vector in the XY plane.

    Examples
    --------
    >>>

    """
    return [u[0] / v[0], u[1] / v[1], 0.0]


# ==============================================================================
# ...
# ==============================================================================


def cross_vectors(u, v):
    r"""Compute the cross product of two vectors.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the second vector.

    Returns
    -------
    [float, float, float]
        The cross product of the two vectors.

    Notes
    -----
    The xyz components of the cross product of two vectors :math:`\mathbf{u}`
    and :math:`\mathbf{v}` can be computed as the *minors* of the following matrix:

    .. math::
       :nowrap:

        \begin{bmatrix}
        x & y & z \\
        u_{x} & u_{y} & u_{z} \\
        v_{x} & v_{y} & v_{z}
        \end{bmatrix}

    Therefore, the cross product can be written as:

    .. math::
       :nowrap:

        \begin{eqnarray}
            \mathbf{u} \times \mathbf{v}
            & =
            \begin{bmatrix}
            u_{y} * v_{z} - u_{z} * v_{y} \\
            u_{z} * v_{x} - u_{x} * v_{z} \\
            u_{x} * v_{y} - u_{y} * v_{x}
            \end{bmatrix}
        \end{eqnarray}

    Examples
    --------
    >>> cross_vectors([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
    [0.0, 0.0, 1.0]

    """
    return [
        u[1] * v[2] - u[2] * v[1],
        u[2] * v[0] - u[0] * v[2],
        u[0] * v[1] - u[1] * v[0],
    ]


def cross_vectors_xy(u, v):
    """Compute the cross product of two vectors, assuming they lie in the XY-plane.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) coordinates of the first vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) coordinates of the second vector.

    Returns
    -------
    [float, float, float]
        The cross product of the two vectors.
        This vector will be perpendicular to the XY plane.

    Examples
    --------
    >>> cross_vectors_xy([1.0, 0.0], [0.0, 1.0])
    [0.0, 0.0, 1.0]

    >>> cross_vectors_xy([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
    [0.0, 0.0, 1.0]

    >>> cross_vectors_xy([1.0, 0.0, 1.0], [0.0, 1.0, 1.0])
    [0.0, 0.0, 1.0]

    """
    return [0.0, 0.0, u[0] * v[1] - u[1] * v[0]]


def dot_vectors(u, v):
    """Compute the dot product of two vectors.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the second vector.

    Returns
    -------
    float
        The dot product of the two vectors.

    Examples
    --------
    >>> dot_vectors([1.0, 0, 0], [2.0, 0, 0])
    2.0

    """
    return sum(a * b for a, b in zip(u, v))


def dot_vectors_xy(u, v):
    """Compute the dot product of two vectors, assuming they lie in the XY-plane.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) coordinates of the first vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) coordinates of the second vector.

    Returns
    -------
    float
        The dot product of the XY components of the two vectors.

    Examples
    --------
    >>> dot_vectors_xy([1.0, 0], [2.0, 0])
    2.0

    >>> dot_vectors_xy([1.0, 0, 0], [2.0, 0, 0])
    2.0

    >>> dot_vectors_xy([1.0, 0, 1], [2.0, 0, 1])
    2.0

    """
    return u[0] * v[0] + u[1] * v[1]


def vector_component(u, v):
    """Compute the component of u in the direction of v.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the direction.

    Returns
    -------
    [float, float, float]
        The component of u in the direction of v.

    Notes
    -----
    This is similar to computing direction cosines, or to the projection of
    a vector onto another vector. See the respective Wikipedia pages ([1]_, [2]_)
    for more info.

    References
    ----------
    .. [1] *Direction cosine*. Available at https://en.wikipedia.org/wiki/Direction_cosine.
    .. [2] *Vector projection*. Available at https://en.wikipedia.org/wiki/Vector_projection.

    Examples
    --------
    >>> vector_component([1.0, 2.0, 3.0], [1.0, 0.0, 0.0])
    [1.0, 0.0, 0.0]

    """
    l2 = length_vector_sqrd(v)
    if not l2:
        return [0, 0, 0]
    x = dot_vectors(u, v) / l2
    return scale_vector(v, x)


def vector_component_xy(u, v):
    """Compute the component of u in the direction of v, assuming they lie in the XY-plane.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the direction.

    Returns
    -------
    [float, float, 0.0]
        The component of u in the XY plane, in the direction of v.

    Notes
    -----
    This is similar to computing direction cosines, or to the projection of
    a vector onto another vector. See the respective Wikipedia pages ([1]_, [2]_)
    for more info.

    References
    ----------
    .. [1] *Direction cosine*. Available at https://en.wikipedia.org/wiki/Direction_cosine.
    .. [2] *Vector projection*. Available at https://en.wikipedia.org/wiki/Vector_projection.

    Examples
    --------
    >>> vector_component_xy([1, 2, 0], [1, 0, 0])
    [1.0, 0.0, 0.0]

    """
    l2 = length_vector_sqrd_xy(v)
    if not l2:
        return [0, 0, 0]
    x = dot_vectors_xy(u, v) / l2
    return scale_vector_xy(v, x)


# ==============================================================================
# these involve vectors interpreted as matrices (lists of lists)
# -> matrix multiplication
# ==============================================================================


def transpose_matrix(M):
    """Transpose a matrix.

    Parameters
    ----------
    M : list[list[float]] | :class:`~compas.geometry.Transformation`
        The matrix to be transposed.

    Returns
    -------
    list[list[float]]
        The result matrix.

    """
    return list(map(list, zip(*list(M))))


def multiply_matrices(A, B):
    r"""Mutliply a matrix with a matrix.

    Parameters
    ----------
    A : list[list[float]] | :class:`~compas.geometry.Transformation`
        The first matrix.
    B : list[list[float]] | :class:`~compas.geometry.Transformation`
        The second matrix.

    Returns
    -------
    list[list[float]]
        The result matrix.

    Raises
    ------
    Exception
        If the shapes of the matrices are not compatible.
        If the row length of B is inconsistent.

    Notes
    -----
    This is a pure Python version of the following linear algebra procedure:

    .. math::

        \mathbf{A} \cdot \mathbf{B} = \mathbf{C}

    with :math:`\mathbf{A}` [m x n], :math:`\mathbf{B}` [n x o], and :math:`\mathbf{C}` [m x o].

    Examples
    --------
    >>> A = [[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]]
    >>> B = [[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]]
    >>> multiply_matrices(A, B)
    [[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]]

    """
    A = list(A)
    B = list(B)
    n = len(B)  # number of rows in B
    o = len(B[0])  # number of cols in B
    if not all(len(row) == o for row in B):
        raise Exception("Row length in matrix B is inconsistent.")
    if not all([len(row) == n for row in A]):
        raise Exception("Matrix shapes are not compatible.")
    B = list(zip(*list(B)))
    return [[dot_vectors(row, col) for col in B] for row in A]


def multiply_matrix_vector(A, b):
    r"""Multiply a matrix with a vector.

    Parameters
    ----------
    A : list[list[float]] | :class:`~compas.geometry.Transformation`
        The matrix.
    b : [float, float, float] | :class:`~compas.geometry.Vector`
        The vector.

    Returns
    -------
    [float, float, float]
        The resulting vector.

    Raises
    ------
    Exception
        If not all rows of the matrix have the same length as the vector.

    Notes
    -----
    This is a Python version of the following linear algebra procedure:

    .. math::

        \mathbf{A} \cdot \mathbf{x} = \mathbf{b}

    with :math:`\mathbf{A}` a *m* by *n* matrix, :math:`\mathbf{x}` a vector of
    length *n*, and :math:`\mathbf{b}` a vector of length *m*.

    Examples
    --------
    >>> matrix = [[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]]
    >>> vector = [1.0, 2.0, 3.0]
    >>> multiply_matrix_vector(matrix, vector)
    [2.0, 4.0, 6.0]

    """
    n = len(b)
    if not all([len(row) == n for row in A]):
        raise Exception("Matrix shape is not compatible with vector length.")
    return [dot_vectors(row, b) for row in A]


# ==============================================================================
# linalg
# ==============================================================================


def homogenize_vectors(vectors, w=1.0):
    """Homogenise a list of vectors.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.
    w : float, optional
        Homogenisation parameter.

    Returns
    -------
    list[[float, float, float]]
        Homogenised vectors.

    Notes
    -----
    Vectors described by XYZ components are homogenised by appending a homogenisation
    parameter to the components, and by dividing each component by that parameter.
    Homogenisatioon of vectors is often used in relation to transformations.

    Examples
    --------
    >>> vectors = [[1.0, 0.0, 0.0]]
    >>> homogenize_vectors(vectors)
    [[1.0, 0.0, 0.0, 1.0]]

    """
    return [[x / w, y / w, z / w, w] for x, y, z in vectors]


def dehomogenize_vectors(vectors):
    """Dehomogenise a list of vectors.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors.

    Returns
    -------
    list[float, float, float]
        Dehomogenised vectors.

    Examples
    --------
    >>>

    """
    return [[x * w, y * w, z * w] for x, y, z, w in vectors]


def orthonormalize_vectors(vectors):
    """Orthonormalize a set of vectors.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        The set of vectors to othonormalize.

    Returns
    -------
    list[[float, float, float]]
        An othonormal basis for the input vectors.

    Notes
    -----
    This creates a basis for the range (column space) of the matrix A.T,
    with A = vectors.

    Orthonormalisation is according to the Gram-Schmidt process.

    Examples
    --------
    >>> orthonormalize_vectors([[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
    [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]

    """
    basis = []
    for v in vectors:
        if basis:
            e = subtract_vectors(v, sum_vectors([vector_component(v, b) for b in basis]))
        else:
            e = v
        if any(axis > 1e-10 for axis in e):
            basis.append(normalize_vector(e))
    return basis


# src/compas/geometry/_core/analytical.py

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

from math import exp
from math import cos
from math import sin


def circle_evaluate(t, r, z=0):
    """Evalutes a circle at a parameter.

    Parameters
    ----------
    t: float
        Parameter.
    r: float
        Radius of the circle.
    z: float, optional
        Elevation of the circle above the XY plane.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates.

    """
    return [r * cos(t), r * sin(t), z]


def ellipse_evaluate(t, a, b, z=0):
    """Evalutes an ellipse at a parameter.

    Parameters
    ----------
    t: float
        Parameter.
    a: float
        Ellipse major.
    b: float
        Ellipse minor.
    z: float, optional
        Elevation of the ellipse above the XY plane.

    Notes
    -----
    The radius along the x and y axes are a and b, respectively.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates.

    """
    return [a * cos(t), b * sin(t), z]


def archimedean_spiral_evaluate(t, a, b, z=0):
    """Evalutes a spiral at a parameter.

    The analytical polar equation is r = a + b * theta.

    Parameters
    ----------
    t: float
        Parameter.
    a: float
        The angle of the tangent at the beginning is equal to a.
    b: float
        The radius between turns is equal to 2 * pi * b.
    z: float, optional
        Elevation of the spiral segment above the XY plane.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates.

    References
    ----------
    * GeoGebra. *Archimedean Spiral built by parametric equations*. Available at: https://www.geogebra.org/m/dZuH5hWa.

    """
    return [b * t * cos(t + a), b * t * sin(t + a), z]


def logarithmic_spiral_evaluate(t, a, b, z=0):
    """Evalutes a logarithmic spiral at a parameter.

    The analytical polar equation is r = a * exp(b * theta).

    Parameters
    ----------
    t: float
        Parameter.
    a: float
        The angle of the tangent at the beginning is equal to a.
    b: float
        The radius between turns is equal to 2 * pi * b.
    z: float, optional
        Elevation of the spiral segment above the XY plane.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates.

    References
    ----------
    * GeoGebra. *An equiangular spiral - parametric equation*. Available at: https://www.geogebra.org/m/zsHgCvq7.

    """
    return [a * exp(b * t) * cos(t), a * exp(b * t) * sin(t), z]


def helix_evaluate(t, a, b):
    """Evalutes an helix at a parameter.

    Parameters
    ----------
    t: float
        Parameter.
    a: float
        The radius of the helix is a.
    b: float
        The slope of the helix is b / a.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates.

    Notes
    -----
    An interpretation of the constants a and b are the radius of the helix is a, and the slope of the helix is b / a.

    References
    ----------
    .. [1] Wolfram MathWorld. *Helix*.
               Available at: http://mathworld.wolfram.com/Helix.html.
    """
    return [a * cos(t), a * sin(t), b * t]


# src/compas/geometry/_core/angles.py

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

from math import pi
from math import degrees
from math import acos

from ._algebra import subtract_vectors
from ._algebra import subtract_vectors_xy
from ._algebra import dot_vectors
from ._algebra import dot_vectors_xy
from ._algebra import length_vector
from ._algebra import length_vector_xy
from ._algebra import cross_vectors


def angle_vectors(u, v, deg=False, tol=0.0):
    """Compute the smallest angle between two vectors.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the second vector.
    deg : bool, optional
        If True, returns the angle in degrees.
    tol : float, optional
        Tolerance for the length of the vectors.

    Returns
    -------
    float
        The smallest angle in radians (in degrees if ``deg == True``).
        The angle is always positive.

    Examples
    --------
    >>> angle_vectors([0.0, 1.0, 0.0], [1.0, 0.0, 0.0])
    1.57079

    """
    L = length_vector(u) * length_vector(v)
    if tol and L < tol:
        return 0
    a = dot_vectors(u, v) / L
    a = max(min(a, 1), -1)

    if deg:
        return degrees(acos(a))
    return acos(a)


def angle_vectors_signed(u, v, normal, deg=False, threshold=1e-3):
    """Computes the signed angle between two vectors.

    It calculates the angle such that rotating vector u about the normal by
    angle would result in a vector that looks into the same direction as v.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the second vector.
    normal : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the plane's normal spanned by u and v.
    deg : bool, optional
        If True, returns the angle in degrees.
    threshold : float, optional
        The threshold (radians) used to consider if the angle is zero.

    Returns
    -------
    float
        The signed angle in radians (in degrees if deg == True).

    Examples
    --------
    >>> normal = [0.0, 0.0, 1.0]
    >>> angle_vectors_signed([0.0, 1.0, 0.0], [1.0, 0.0, 0.0], normal)
    -1.57079
    """
    angle = angle_vectors(u, v)
    normal_uv = cross_vectors(u, v)

    if length_vector(normal_uv) > threshold:
        # check if normal_uv has the same direction as normal
        angle_btw_normals = angle_vectors(normal, normal_uv)
        if angle_btw_normals > threshold:
            angle *= -1

    if deg:
        return degrees(angle)
    else:
        return angle


def angle_vectors_xy(u, v, deg=False, tol=1e-4):
    """Compute the smallest angle between the XY components of two vectors.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The first 2D or 3D vector (Z will be ignored).
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        The second 2D or 3D vector (Z will be ignored).
    deg : bool, optional
        If True, returns the angle in degrees.
    tol : float, optional
        Tolerance for the length of the vectors.

    Returns
    -------
    float
        The smallest angle in radians (in degrees if ``deg == True``).
        The angle is always positive.

    Examples
    --------
    >>>

    """
    L = length_vector_xy(u) * length_vector_xy(v)
    if L < tol:
        return 0
    a = dot_vectors_xy(u, v) / L
    a = max(min(a, 1), -1)
    if deg:
        return degrees(acos(a))
    return acos(a)


def angle_points(a, b, c, deg=False):
    r"""Compute the smallest angle between the vectors defined by three points.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    c : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    deg : bool, optional
        If True, returns the angle in degrees.

    Returns
    -------
    float
        The smallest angle in radians (in degrees if ``deg == True``).
        The angle is always positive.

    Notes
    -----
    The vectors are defined in the following way

    .. math::

        \mathbf{u} = \mathbf{b} - \mathbf{a} \\
        \mathbf{v} = \mathbf{c} - \mathbf{a}

    Z components may be provided, but are simply ignored.

    """
    u = subtract_vectors(b, a)
    v = subtract_vectors(c, a)
    return angle_vectors(u, v, deg)


def angle_points_xy(a, b, c, deg=False):
    r"""Compute the smallest angle between the vectors defined by the XY components of three points.

    Parameters
    ----------
    a : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    b : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    c : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    deg : bool, optional
        If True, returns the angle in degrees.

    Returns
    -------
    float
        The smallest angle in radians (in degrees if ``deg == True``).
        The angle is always positive.

    Notes
    -----
    The vectors are defined in the following way

    .. math::

        \mathbf{u} = \mathbf{b} - \mathbf{a} \\
        \mathbf{v} = \mathbf{c} - \mathbf{a}

    Z components may be provided, but are simply ignored.

    """
    u = subtract_vectors_xy(b, a)
    v = subtract_vectors_xy(c, a)
    return angle_vectors_xy(u, v, deg)


def angles_vectors(u, v, deg=False):
    """Compute the the 2 angles formed by a pair of vectors.

    Parameters
    ----------
    u : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the first vector.
    v : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ components of the second vector.
    deg : bool, optional
        If True, returns the angle in degrees.

    Returns
    -------
    float
        The smallest angle in radians, or in degrees if ``deg == True``.
    float
        The other angle.

    Examples
    --------
    >>>

    """
    if deg:
        a = angle_vectors(u, v, deg)
        return a, 360.0 - a
    a = angle_vectors(u, v)
    return a, pi * 2 - a


def angles_vectors_xy(u, v, deg=False):
    """Compute the angles between the XY components of two vectors.

    Parameters
    ----------
    u : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) coordinates of the first vector.
    v : [float, float] or [float, float, float] | :class:`~compas.geometry.Vector`
        XY(Z) coordinates of the second vector.
    deg : bool, optional
        If True, returns the angle in degrees.

    Returns
    -------
    float
        The smallest angle in radians, or in degrees if ``deg == True``.
    float
        The other angle.

    Notes
    -----
    Z components may be provided, but are simply ignored.

    Examples
    --------
    >>>

    """
    if deg:
        a = angle_vectors_xy(u, v, deg)
        return a, 360.0 - a
    a = angle_vectors_xy(u, v)
    return a, pi * 2 - a


def angles_points(a, b, c, deg=False):
    r"""Compute the two angles between two vectors defined by three points.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    c : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    deg : bool, optional
        If True, returns the angle in degrees.

    Returns
    -------
    float
        The smallest angle in radians, or in degrees if ``deg == True``.
    float
        The other angle.

    Notes
    -----
    The vectors are defined in the following way

    .. math::

        \mathbf{u} = \mathbf{b} - \mathbf{a} \\
        \mathbf{v} = \mathbf{c} - \mathbf{a}

    Examples
    --------
    >>>

    """
    u = subtract_vectors(b, a)
    v = subtract_vectors(c, a)
    return angles_vectors(u, v, deg)


def angles_points_xy(a, b, c, deg=False):
    r"""Compute the two angles between the two vectors defined by the XY components of three points.

    Parameters
    ----------
    a : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates.
    b : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates.
    c : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates.
    deg : bool, optional
        If True, returns the angle in degrees.

    Returns
    -------
    float
        The smallest angle in radians, or in degrees if ``deg == True``.
    float
        The other angle.

    Notes
    -----
    The vectors are defined in the following way

    .. math::

        \mathbf{u} = \mathbf{b} - \mathbf{a} \\
        \mathbf{v} = \mathbf{c} - \mathbf{a}

    Z components may be provided, but are simply ignored.

    Examples
    --------
    >>>

    """
    u = subtract_vectors_xy(b, a)
    v = subtract_vectors_xy(c, a)
    return angles_vectors_xy(u, v, deg)


def angle_planes(a, b, deg=False):
    """Compute the smallest angle between the two normal vectors of two planes.

    Parameters
    ----------
    a : [point, vector] | :class:`~compas.geometry.Plane`
        The first plane.
    b : [point, vector] | :class:`~compas.geometry.Plane`
        The second plane.
    deg : bool, optional
        If True, returns the angle in degrees.

    Returns
    -------
    float
        The smallest angle in radians, or in degrees if ``deg == True``.
    float
        The other angle.

    Examples
    --------
    >>> plane_a = [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]
    >>> plane_b = [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]
    >>> angle_planes(plane_a, plane_b, True)
    90.0
    """
    return angle_vectors(a[1], b[1], deg)


# src/compas/geometry/_core/centroids.py

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

from math import fabs

from compas.utilities import pairwise

from ._algebra import add_vectors
from ._algebra import subtract_vectors
from ._algebra import subtract_vectors_xy
from ._algebra import length_vector
from ._algebra import length_vector_xy
from ._algebra import dot_vectors
from ._algebra import cross_vectors
from ._algebra import cross_vectors_xy
from ._algebra import scale_vector
from ._algebra import sum_vectors


def midpoint_point_point(a, b):
    """Compute the midpoint of two points.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the first point.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the second point.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the midpoint.

    """
    return [0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])]


def midpoint_point_point_xy(a, b):
    """Compute the midpoint of two points lying in the XY-plane.

    Parameters
    ----------
    a : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the first 2D or 3D point (Z will be ignored).
    b : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the second 2D or 3D point (Z will be ignored).

    Returns
    -------
    [float, float, 0.0]
        XYZ coordinates of the midpoint in the XY plane.
    """
    return [0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.0]


def midpoint_line(line):
    """Compute the midpoint of a line defined by two points.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        XYZ coordinates of the first point, and XYZ coordinates of the second point.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the midpoint.

    Examples
    --------
    >>> midpoint_line(([0.0, 0.0, 0.0], [1.0, 0.0, 1.0]))
    [0.5, 0.0, 0.5]
    """
    return midpoint_point_point(*line)


def midpoint_line_xy(line):
    """Compute the midpoint of a line defined by two points.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        XYZ coordinates of the first point, and XYZ coordinates of the second point.

    Returns
    -------
    [float, float, 0.0]
        XYZ coordinates of the midpoint in the XY plane.

    Examples
    --------
    >>> midpoint_line_xy(([0.0, 0.0, 0.0], [1.0, 0.0, 1.0]))
    [0.5, 0.0, 0.0]
    """
    return midpoint_point_point_xy(*line)


def centroid_points(points):
    """Compute the centroid of a set of points.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of XYZ coordinates.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the centroid.

    Warnings
    --------
    Duplicate points are **NOT** removed. If there are duplicates in the
    sequence, they should be there intentionally.

    Examples
    --------
    >>> points = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]
    >>> centroid_points(points)
    [0.5, 0.5, 0.0]
    """
    p = len(points)
    x, y, z = zip(*points)
    return [sum(x) / p, sum(y) / p, sum(z) / p]


def centroid_points_weighted(points, weights):
    """Compute the weighted centroid of a set of points. The weights can be any between minus and plus infinity.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of point coordinates.
    weights : sequence[float]
        A list of weight floats.

    Returns
    -------
    [float, float, float]
        The coordinates of the weighted centroid.
    """
    vectors = [scale_vector(point, weight) for point, weight in zip(points, weights)]
    vector = scale_vector(sum_vectors(vectors), 1.0 / sum(weights))
    return vector


def centroid_points_xy(points):
    """Compute the centroid of a set of points lying in the XY-plane.

    Parameters
    ----------
    points : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of points represented by their XY(Z) coordinates.

    Returns
    -------
    [float, float, 0.0]
        XYZ coordinates of the centroid in the XY plane.

    Warnings
    --------
    Duplicate points are **NOT** removed. If there are duplicates in the
    sequence, they should be there intentionally.

    Examples
    --------
    >>> points = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]
    >>> centroid_points_xy(points)
    [0.5, 0.5, 0.0]
    """
    p = len(points)
    x, y = list(zip(*points))[:2]
    return [sum(x) / p, sum(y) / p, 0.0]


def centroid_polygon(polygon):
    r"""Compute the centroid of the surface of a polygon.

    Parameters
    ----------
    polygon : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of polygon point coordinates.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates of the centroid.

    Notes
    -----
    The centroid is the centre of gravity of the polygon surface if mass would be
    uniformly distributed over it.

    It is calculated by triangulating the polygon surface with respect to the centroid
    of the polygon vertices, and then computing the centroid of the centroids of
    the individual triangles, weighted by the corresponding triangle area in
    proportion to the total surface area.

    .. math::

        c_x = \frac{1}{A} \sum_{i=1}^{N} A_i \cdot c_{x,i}
        c_y = \frac{1}{A} \sum_{i=1}^{N} A_i \cdot c_{y,i}
        c_z = \frac{1}{A} \sum_{i=1}^{N} A_i \cdot c_{z,i}

    Warnings
    --------
    The polygon need not be convex.

    The polygon need not be flat. However, it is unclear what the meaning of the
    centroid is in that case.

    The polygon may be self-intersecting. However, it is unclear what the meaning
    of the centroid is in that case.

    Examples
    --------
    >>> polygon = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]
    >>> centroid_polygon(polygon)
    [0.5, 0.5, 0.0]
    """
    p = len(polygon)

    assert p > 2, "At least three points required"

    if p == 3:
        return centroid_points(polygon)

    o = centroid_points(polygon)
    a = polygon[-1]
    b = polygon[0]
    oa = subtract_vectors(a, o)
    ob = subtract_vectors(b, o)
    n0 = cross_vectors(oa, ob)

    x, y, z = centroid_points([o, a, b])
    a2 = length_vector(n0)

    A2 = a2
    cx = a2 * x
    cy = a2 * y
    cz = a2 * z

    for i in range(1, p):
        a = b
        b = polygon[i]

        oa = ob
        ob = subtract_vectors(b, o)

        n = cross_vectors(oa, ob)
        x, y, z = centroid_points([o, a, b])

        if dot_vectors(n, n0) > 0:
            a2 = length_vector(n)
        else:
            a2 = -length_vector(n)

        A2 += a2
        cx += a2 * x
        cy += a2 * y
        cz += a2 * z

    if A2 == 0:
        return polygon[0]

    return [cx / A2, cy / A2, cz / A2]


def centroid_polygon_xy(polygon):
    r"""Compute the centroid of the surface of a polygon projected to the XY plane.

    Parameters
    ----------
    polygon : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of polygon point XY(Z) coordinates.
        The Z coordinates are ignored.

    Returns
    -------
    [float, float, 0.0]
        The XYZ coordinates of the centroid in the XY plane.

    Notes
    -----
    The centroid is the centre of gravity of the polygon surface if mass would be
    uniformly distributed over it.

    It is calculated by triangulating the polygon surface with respect to the centroid
    of the polygon vertices, and then computing the centroid of the centroids of
    the individual triangles, weighted by the corresponding triangle area in
    proportion to the total surface area.

    .. math::

        c_x = \frac{1}{A} \sum_{i=1}^{N} A_i \cdot c_{x,i}
        c_y = \frac{1}{A} \sum_{i=1}^{N} A_i \cdot c_{y,i}
        c_z = 0

    Warnings
    --------
    The polygon need not be convex.

    The polygon may be self-intersecting. However, it is unclear what the meaning
    of the centroid is in that case.

    Examples
    --------
    >>> polygon = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]
    >>> centroid_polygon_xy(polygon)
    [0.5, 0.5, 0.0]
    """
    p = len(polygon)

    assert p > 2, "At least three points required"

    if p == 3:
        return centroid_points_xy(polygon)

    o = centroid_points_xy(polygon)
    a = polygon[-1]
    b = polygon[0]
    oa = subtract_vectors_xy(a, o)
    ob = subtract_vectors_xy(b, o)
    n0 = cross_vectors_xy(oa, ob)

    x, y, z = centroid_points_xy([o, a, b])
    a2 = fabs(n0[2])

    A2 = a2
    cx = a2 * x
    cy = a2 * y

    for i in range(1, p):
        a = b
        b = polygon[i]

        oa = ob
        ob = subtract_vectors_xy(b, o)

        n = cross_vectors_xy(oa, ob)
        x, y, z = centroid_points_xy([o, a, b])

        if n[2] * n0[2] > 0:
            a2 = fabs(n[2])
        else:
            a2 = -fabs(n[2])

        A2 += a2
        cx += a2 * x
        cy += a2 * y

    return [cx / A2, cy / A2, 0.0]


def centroid_polygon_vertices(polygon):
    """Compute the centroid of the vertices of a polygon.

    Parameters
    ----------
    polygon : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of polygon point coordinates.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates of the centroid.
    """
    return centroid_points(polygon)


def centroid_polygon_vertices_xy(polygon):
    """Compute the centroid of the vertices of a polygon projected to the XY plane.

    Parameters
    ----------
    polygon : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of polygon point coordinates.
        The Z coordinates will be ignored.

    Returns
    -------
    [float, float, 0.0]
        The XYZ coordinates of the centroid in the XY plane.

    """
    return centroid_points_xy(polygon)


def centroid_polygon_edges(polygon):
    """Compute the centroid of the edges of a polygon.

    Parameters
    ----------
    polygon : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of polygon point coordinates.

    Returns
    -------
    [float, float, float]
        The XYZ coordinates of the centroid.

    Notes
    -----
    The centroid of the edges is the centroid of the midpoints of the edges, with
    each midpoint weighted by the length of the corresponding edge proportional
    to the total length of the boundary.
    """
    L = 0
    cx = 0
    cy = 0
    cz = 0
    p = len(polygon)
    for i in range(-1, p - 1):
        p1 = polygon[i]
        p2 = polygon[i + 1]
        d = length_vector(subtract_vectors(p2, p1))
        cx += 0.5 * d * (p1[0] + p2[0])
        cy += 0.5 * d * (p1[1] + p2[1])
        cz += 0.5 * d * (p1[2] + p2[2])
        L += d
    return [cx / L, cy / L, cz / L]


def centroid_polygon_edges_xy(polygon):
    """Compute the centroid of the edges of a polygon prohected to the XY plane.

    Parameters
    ----------
    polygon : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Point`]
        A sequence of polygon point coordinates.
        The Z coordinates will be ignored.

    Returns
    -------
    [float, float, 0.0]
        The XYZ coordinates of the centroid in the XY plane.

    Notes
    -----
    The centroid of the edges is the centroid of the midpoints of the edges, with
    each midpoint weighted by the length of the corresponding edge proportional
    to the total length of the boundary.
    """
    L = 0
    cx = 0
    cy = 0
    p = len(polygon)
    for i in range(-1, p - 1):
        p1 = polygon[i]
        p2 = polygon[i + 1]
        d = length_vector_xy(subtract_vectors_xy(p2, p1))
        cx += 0.5 * d * (p1[0] + p2[0])
        cy += 0.5 * d * (p1[1] + p2[1])
        L += d
    return [cx / L, cy / L, 0.0]


def centroid_polyhedron(polyhedron):
    """Compute the center of mass of a polyhedron.

    Parameters
    ----------
    polyhedron : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[sequence[int]]]
        The coordinates of the vertices,
        and the indices of the vertices forming the faces.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the center of mass.

    Warnings
    --------
    This function assumes that the vertex cycles of the faces are such that the
    face normals are consistently pointing outwards, resulting in a *positive*
    polyhedron.

    Examples
    --------
    >>> from compas.geometry import Polyhedron
    >>> p = Polyhedron.from_platonicsolid(6)
    >>> centroid_polyhedron(p)
    [0.0, 0.0, 0.0]
    """
    vertices, faces = polyhedron

    V = 0
    x = 0.0
    y = 0.0
    z = 0.0
    ex = [1.0, 0.0, 0.0]
    ey = [0.0, 1.0, 0.0]
    ez = [0.0, 0.0, 1.0]

    for face in faces:
        if len(face) == 3:
            triangles = [face]
        else:
            centroid = centroid_points([vertices[index] for index in face])
            w = len(vertices)
            vertices.append(centroid)
            triangles = [[w, u, v] for u, v in pairwise(face + face[0:1])]

        for triangle in triangles:
            a = vertices[triangle[0]]
            b = vertices[triangle[1]]
            c = vertices[triangle[2]]
            ab = subtract_vectors(b, a)
            ac = subtract_vectors(c, a)
            n = cross_vectors(ab, ac)
            V += dot_vectors(a, n)

            nx = dot_vectors(n, ex)
            ny = dot_vectors(n, ey)
            nz = dot_vectors(n, ez)

            ab = add_vectors(a, b)
            bc = add_vectors(b, c)
            ca = add_vectors(c, a)

            ab_x2 = dot_vectors(ab, ex) ** 2
            bc_x2 = dot_vectors(bc, ex) ** 2
            ca_x2 = dot_vectors(ca, ex) ** 2

            x += nx * (ab_x2 + bc_x2 + ca_x2)

            ab_y2 = dot_vectors(ab, ey) ** 2
            bc_y2 = dot_vectors(bc, ey) ** 2
            ca_y2 = dot_vectors(ca, ey) ** 2

            y += ny * (ab_y2 + bc_y2 + ca_y2)

            ab_z2 = dot_vectors(ab, ez) ** 2
            bc_z2 = dot_vectors(bc, ez) ** 2
            ca_z2 = dot_vectors(ca, ez) ** 2

            z += nz * (ab_z2 + bc_z2 + ca_z2)

            # for j in (-1, 0, 1):
            #     ab = add_vectors(vertices[triangle[j]], vertices[triangle[j + 1]])
            #     x += nx * dot_vectors(ab, ex) ** 2
            #     y += ny * dot_vectors(ab, ey) ** 2
            #     z += nz * dot_vectors(ab, ez) ** 2

    V = V / 6.0

    if V < 1e-9:
        d = 1.0 / (2 * 24)
    else:
        d = 1.0 / (2 * 24 * V)

    x *= d
    y *= d
    z *= d

    return [x, y, z]


# src/compas/geometry/_core/constructors.py

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

from math import sqrt

from ._algebra import subtract_vectors
from ._algebra import sum_vectors
from ._algebra import cross_vectors
from ._algebra import dot_vectors
from ._algebra import scale_vector
from ._algebra import normalize_vector
from ._algebra import length_vector
from ._algebra import length_vector_sqrd


def circle_from_points(a, b, c):
    """Construct a circle from three points.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    c : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.

    Returns
    -------
    ([float, float, float], [float, float, float]), float
        Center, normal and radius of the circle respectively.

    Notes
    -----
    For more information, see [1]_.

    References
    ----------
    .. [1] Wikipedia. *Circumscribed circle*.
           Available at: https://en.wikipedia.org/wiki/Circumscribed_circle.

    Examples
    --------
    >>>

    """
    ab = subtract_vectors(b, a)
    cb = subtract_vectors(b, c)
    ba = subtract_vectors(a, b)
    ca = subtract_vectors(a, c)
    ac = subtract_vectors(c, a)
    bc = subtract_vectors(c, b)
    normal = normalize_vector(cross_vectors(ab, ac))
    d = 2 * length_vector_sqrd(cross_vectors(ba, cb))
    A = length_vector_sqrd(cb) * dot_vectors(ba, ca) / d
    B = length_vector_sqrd(ca) * dot_vectors(ab, cb) / d
    C = length_vector_sqrd(ba) * dot_vectors(ac, bc) / d
    Aa = scale_vector(a, A)
    Bb = scale_vector(b, B)
    Cc = scale_vector(c, C)
    center = sum_vectors([Aa, Bb, Cc])
    radius = length_vector(subtract_vectors(a, center))
    return (center, normal), radius


def circle_from_points_xy(a, b, c):
    """Create a circle from three points lying in the XY-plane

    Parameters
    ----------
    a : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    b : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    c : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).

    Returns
    -------
    ([float, float, float], [float, float, float]), float
        Center, normal and radius of the circle respectively.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wikipedia. *Circumscribed circle*.
           Available at: https://en.wikipedia.org/wiki/Circumscribed_circle.

    Examples
    --------
    >>>

    """
    ax, ay = a[0], a[1]
    bx, by = b[0], b[1]
    cx, cy = c[0], c[1]
    a = bx - ax
    b = by - ay
    c = cx - ax
    d = cy - ay
    e = a * (ax + bx) + b * (ay + by)
    f = c * (ax + cx) + d * (ay + cy)
    g = 2 * (a * (cy - by) - b * (cx - bx))
    if g == 0:
        return None
    centerx = (d * e - b * f) / g
    centery = (a * f - c * e) / g
    radius = sqrt((ax - centerx) ** 2 + (ay - centery) ** 2)
    return ([centerx, centery, 0.0], [0.0, 0.0, 1.0]), radius


# src/compas/geometry/_core/distance.py

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

from math import fabs
from math import sqrt

from compas.utilities import pairwise

from ._algebra import add_vectors
from ._algebra import add_vectors_xy
from ._algebra import subtract_vectors
from ._algebra import subtract_vectors_xy
from ._algebra import scale_vector
from ._algebra import normalize_vector
from ._algebra import length_vector
from ._algebra import length_vector_xy
from ._algebra import length_vector_sqrd
from ._algebra import length_vector_sqrd_xy
from ._algebra import cross_vectors
from ._algebra import cross_vectors_xy
from ._algebra import dot_vectors
from ._algebra import vector_component
from ._algebra import vector_component_xy


def distance_point_point(a, b):
    """Compute the distance bewteen a and b.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of point a.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of point b.

    Returns
    -------
    float
        Distance bewteen a and b.

    Examples
    --------
    >>> distance_point_point([0.0, 0.0, 0.0], [2.0, 0.0, 0.0])
    2.0

    See Also
    --------
    distance_point_point_xy

    """
    ab = subtract_vectors(b, a)
    return length_vector(ab)


def distance_point_point_xy(a, b):
    """Compute the distance between points a and b, assuming they lie in the XY plane.

    Parameters
    ----------
    a : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    b : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).

    Returns
    -------
    float
        Distance between a and b in the XY-plane.

    Examples
    --------
    >>> distance_point_point_xy([0.0, 0.0], [2.0, 0.0])
    2.0

    >>> distance_point_point_xy([0.0, 0.0, 0.0], [2.0, 0.0, 0.0])
    2.0

    >>> distance_point_point_xy([0.0, 0.0, 1.0], [2.0, 0.0, 1.0])
    2.0

    """
    ab = subtract_vectors_xy(b, a)
    return length_vector_xy(ab)


def distance_point_point_sqrd(a, b):
    """Compute the squared distance bewteen points a and b.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of point a.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of point b.

    Returns
    -------
    float
        Squared distance bewteen a and b.

    Examples
    --------
    >>> distance_point_point_sqrd([0.0, 0.0, 0.0], [2.0, 0.0, 0.0])
    4.0

    See Also
    --------
    distance_point_point_sqrd_xy

    """
    ab = subtract_vectors(b, a)
    return length_vector_sqrd(ab)


def distance_point_point_sqrd_xy(a, b):
    """Compute the squared distance between points a and b lying in the XY plane.

    Parameters
    ----------
    a : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the first point.
    b : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the second point.

    Returns
    -------
    float
        Squared distance between a and b in the XY-plane.

    Examples
    --------
    >>> distance_point_point_sqrd_xy([0.0, 0.0], [2.0, 0.0])
    4.0

    >>> distance_point_point_sqrd_xy([0.0, 0.0, 0.0], [2.0, 0.0, 0.0])
    4.0

    >>> distance_point_point_sqrd_xy([0.0, 0.0, 1.0], [2.0, 0.0, 1.0])
    4.0

    """
    ab = subtract_vectors_xy(b, a)
    return length_vector_sqrd_xy(ab)


def distance_point_line(point, line):
    """Compute the distance between a point and a line.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        Point location.
    line : [point, point] | :class:`~compas.geometry.Line`
        Line defined by two points.

    Returns
    -------
    float
        The distance between the point and the line.

    Notes
    -----
    This implementation computes the *right angle distance* from a point P to a
    line defined by points A and B as twice the area of the triangle ABP divided
    by the length of AB [1]_.

    References
    ----------
    .. [1] Wikipedia. *Distance from a point to a line*.
           Available at: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line

    Examples
    --------
    >>>

    """
    a, b = line
    ab = subtract_vectors(b, a)
    pa = subtract_vectors(a, point)
    pb = subtract_vectors(b, point)
    length = length_vector(cross_vectors(pa, pb))
    length_ab = length_vector(ab)
    return length / length_ab


def distance_point_line_xy(point, line):
    """Compute the distance between a point and a line, assuming they lie in the XY-plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the point.
    line : [point, point] | :class:`~compas.geometry.Line`
        Line defined by two points.

    Returns
    -------
    float
        The distance between the point and the line.

    Notes
    -----
    This implementation computes the orthogonal distance from a point P to a
    line defined by points A and B as twice the area of the triangle ABP divided
    by the length of AB [1]_.

    References
    ----------
    .. [1] Wikipedia. *Distance from a point to a line*.
           Available at: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line.

    """
    a, b = line
    ab = subtract_vectors_xy(b, a)
    pa = subtract_vectors_xy(a, point)
    pb = subtract_vectors_xy(b, point)
    length = fabs(cross_vectors_xy(pa, pb)[2])
    length_ab = length_vector_xy(ab)
    return length / length_ab


def distance_point_line_sqrd(point, line):
    """Compute the squared distance between a point and a line.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the point.
    line : [point, point] | :class:`~compas.geometry.Line`
        Line defined by two points.

    Returns
    -------
    float
        The squared distance between the point and the line.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wikipedia. *Distance from a point to a line*.
           Available at: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line.

    """
    a, b = line
    ab = subtract_vectors(b, a)
    pa = subtract_vectors(a, point)
    pb = subtract_vectors(b, point)
    length = length_vector_sqrd(cross_vectors(pa, pb))
    length_ab = length_vector_sqrd(ab)
    return length / length_ab


def distance_point_line_sqrd_xy(point, line):
    """Compute the squared distance between a point and a line lying in the XY-plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    line : [point, point] | :class:`~compas.geometry.Line`
        Line defined by two points.

    Returns
    -------
    float
        The squared distance between the point and the line.

    Notes
    -----
    This implementation computes the orthogonal squared distance from a point P to a
    line defined by points A and B as twice the area of the triangle ABP divided
    by the length of AB [1]_.

    References
    ----------
    .. [1] Wikipedia. *Distance from a point to a line*.
           Available at: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line.

    """
    a, b = line
    ab = subtract_vectors_xy(b, a)
    pa = subtract_vectors_xy(a, point)
    pb = subtract_vectors_xy(b, point)
    length = cross_vectors_xy(pa, pb)[2] ** 2
    length_ab = length_vector_sqrd_xy(ab)
    return length / length_ab


def distance_point_plane(point, plane):
    r"""Compute the distance from a point to a plane defined by origin point and normal.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        Point coordinates.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        A point and a vector defining a plane.

    Returns
    -------
    float
        Distance between point and plane.

    Notes
    -----
    The distance from a point to a plane can be computed from the coefficients
    of the equation of the plane and the coordinates of the point [1]_.

    The equation of a plane is

    .. math::

        Ax + By + Cz + D = 0

    where

    .. math::
        :nowrap:

        \begin{align}
            D &= - Ax_0 - Bx_0 - Cz_0 \\
            Q &= (x_0, y_0, z_0) \\
            N &= (A, B, C)
        \end{align}

    with :math:`Q` a point on the plane, and :math:`N` the normal vector at
    that point. The distance of any point :math:`P` to a plane is the
    absolute value of the dot product of the vector from :math:`Q` to :math:`P`
    and the normal at :math:`Q`.

    References
    ----------
    .. [1] Nykamp, D. *Distance from point to plane*.
           Available at: http://mathinsight.org/distance_point_plane.

    Examples
    --------
    >>>

    """
    return fabs(distance_point_plane_signed(point, plane))


def distance_point_plane_signed(point, plane):
    r"""Compute the signed distance from a point to a plane defined by origin point and normal.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        Point coordinates.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        A point and a vector defining a plane.

    Returns
    -------
    float
        Distance between point and plane.

    Notes
    -----
    The distance from a point to a plane can be computed from the coefficients
    of the equation of the plane and the coordinates of the point [1]_.

    The equation of a plane is

    .. math::

        Ax + By + Cz + D = 0

    where

    .. math::
        :nowrap:

        \begin{align}
            D &= - Ax_0 - Bx_0 - Cz_0 \\
            Q &= (x_0, y_0, z_0) \\
            N &= (A, B, C)
        \end{align}

    with :math:`Q` a point on the plane, and :math:`N` the normal vector at
    that point. The distance of any point :math:`P` to a plane is the
    value of the dot product of the vector from :math:`Q` to :math:`P`
    and the normal at :math:`Q`.

    References
    ----------
    .. [1] Nykamp, D. *Distance from point to plane*.
           Available at: http://mathinsight.org/distance_point_plane.

    Examples
    --------
    >>>

    """
    base, normal = plane
    vector = subtract_vectors(point, base)
    return dot_vectors(vector, normal)


def distance_line_line(l1, l2, tol=0.0):
    r"""Compute the shortest distance between two lines.

    Parameters
    ----------
    l1 : [point, point] | :class:`~compas.geometry.Line`
        Two points defining a line.
    l2 : [point, point] | :class:`~compas.geometry.Line`
        Two points defining a line.

    Returns
    -------
    float
        The distance between the two lines.

    Notes
    -----
    The distance is the absolute value of the dot product of a unit vector that
    is perpendicular to the two lines, and the vector between two points on the lines ([1]_, [2]_).

    If each of the lines is defined by two points (:math:`l_1 = (\mathbf{x_1}, \mathbf{x_2})`,
    :math:`l_2 = (\mathbf{x_3}, \mathbf{x_4})`), then the unit vector that is
    perpendicular to both lines is...

    References
    ----------
    .. [1] Weisstein, E.W. *Line-line Distance*.
           Available at: http://mathworld.wolfram.com/Line-LineDistance.html.
    .. [2] Wikipedia. *Skew lines Distance*.
           Available at: https://en.wikipedia.org/wiki/Skew_lines#Distance.

    Examples
    --------
    >>>

    """
    a, b = l1
    c, d = l2
    ab = subtract_vectors(b, a)
    cd = subtract_vectors(d, c)
    ac = subtract_vectors(c, a)
    n = cross_vectors(ab, cd)
    length = length_vector(n)
    if length <= tol:
        return distance_point_point(closest_point_on_line(l1[0], l2), l1[0])
    n = scale_vector(n, 1.0 / length)
    return fabs(dot_vectors(n, ac))


# ==============================================================================
# closest
# ==============================================================================


def sort_points(point, cloud):
    """Sorts points of a pointcloud based on their distance from a given point.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        The XYZ coordinates of the base point.
    cloud : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A sequence locations in three-dimensional space.

    Returns
    -------
    list[[float, [float, float, float], int]]
        A list containing the points of the cloud sorted by their squared distance to the base points.
        Each item in the list contains the squared distance to the base point, the XYZ coordinates
        of the point in the cloud, and the index of the point in the original cloud.

    Notes
    -----
    Check kdTree class for an optimized implementation (MR).

    Examples
    --------
    >>>

    """
    minsq = [distance_point_point_sqrd(p, point) for p in cloud]
    return sorted(zip(minsq, cloud, range(len(cloud))), key=lambda x: x[0])


def sort_points_xy(point, cloud):
    """Sorts points of a pointcloud based on their distance from a given point,
    assuming all points lie in the XY plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point.
    cloud : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Point`]
        A list of points represented by their XY(Z) coordinates.

    Returns
    -------
    list[[float, [float, float, 0.0], int]]
        A list containing the points of the cloud sorted by their squared distance to the base points.
        Each item in the list contains the squared distance to the base point, the XYZ coordinates
        of the point in the cloud in the XY plane, and the index of the point in the original cloud.

    Notes
    -----
    Check kdTree class for an optimized implementation (MR).

    Examples
    --------
    >>>

    """
    minsq = [distance_point_point_sqrd_xy(p, point) for p in cloud]
    return sorted(zip(minsq, cloud, range(len(cloud))), key=lambda x: x[0])


def closest_point_in_cloud(point, cloud):
    """Calculates the closest point in a pointcloud.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the base point.
    cloud : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A sequence locations in three-dimensional space.

    Returns
    -------
    float
        The distance to the closest point.
    [float, float, float]
        XYZ coordinates of the closest point.
    int
        The index of the closest point in the original list.

    Notes
    -----
    Check kdTree class for an optimized implementation.

    Examples
    --------
    >>>

    """
    data = sort_points(point, cloud)
    d, xyz, index = data[0]
    return sqrt(d), xyz, index


def closest_points_in_cloud_numpy(points, cloud, threshold=10**7, distances=True, num_nbrs=1):
    """Find the closest points in a point cloud to a set of sample points.

    Parameters
    ----------
    points : array_like [n x 3]
        The sample points.
    cloud : array_like [n x 3]
        The cloud points to compare to.
    threshold : float, optional
        Only points within this distance are checked.
    distances : bool, optional
        If True, return the distance matrix in addition to the indices of the closest points.
    num_nbrs : int, optional
        The number of nearest neighbors to include in the result.

    Returns
    -------
    list or tuple[list, array]
        If `distances` is False, indices of the closest points in the cloud per point in points.
        If `distances` is True, indices of the closest points in the cloud per point in points
        and distances between points and closest points in cloud (n x n).

    Notes
    -----
    Items in cloud further from items in points than threshold return zero
    distance and will affect the indices returned if not set suitably high.

    Examples
    --------
    >>> from numpy import allclose
    >>> points = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]
    >>> cloud = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]
    >>> cp = closest_points_in_cloud_numpy(points, cloud, distances=True)
    >>> allclose(cp[1], [[0, 1, 1.4142, 1], [1, 0, 1, 1.4142], [1.4142, 1, 0, 1], [1, 1.4142, 1, 0]])
    True

    """
    from numpy import asarray
    from numpy import argmin
    from numpy import argpartition
    from scipy.spatial import distance_matrix

    points = asarray(points).reshape((-1, 3))
    cloud = asarray(cloud).reshape((-1, 3))
    d_matrix = distance_matrix(points, cloud, threshold=threshold)
    if num_nbrs == 1:
        indices = argmin(d_matrix, axis=1)
    else:
        indices = argpartition(d_matrix, num_nbrs, axis=1)
    if distances:
        return indices, d_matrix
    return indices


def closest_point_in_cloud_xy(point, cloud):
    """Calculates the closest point in a list of points in the XY-plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a the base point.
    cloud : sequence[[float, float] or [float, float, float] | :class:`~compas.geometry.Point`]
        A list of points forming the cloud, with each point represented by its XY(Z) coordinates.

    Returns
    -------
    float
        The distance to the closest point.
    [float, float, 0.0]
        The XYZ coordinates of the closest point in the XY plane.
    int
        The index of the closest point in the cloud.

    Notes
    -----
    Check kdTree class for an optimized implementation (MR).

    """
    data = sort_points_xy(point, cloud)
    d, xyz, index = data[0]
    return sqrt(d), xyz, index


def closest_point_on_line(point, line):
    """Computes closest point on line to a given point.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the line.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of closest point.

    Examples
    --------
    >>>

    See Also
    --------
    :func:`basic.transformations.project_point_line`

    """
    a, b = line
    ab = subtract_vectors(b, a)
    ap = subtract_vectors(point, a)
    c = vector_component(ap, ab)
    return add_vectors(a, c)


def closest_point_on_line_xy(point, line):
    """Compute closest point on line (continuous) to a given point lying in the XY-plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two XY(Z) points defining a line.

    Returns
    -------
    [float, float, 0.0]
        XYZ coordinates of the closest point in the XY plane.

    """
    a, b = line
    ab = subtract_vectors_xy(b, a)
    ap = subtract_vectors_xy(point, a)
    c = vector_component_xy(ap, ab)
    return add_vectors_xy(a, c)


def closest_point_on_segment(point, segment):
    """Computes closest point on line segment (p1, p2) to test point.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates.
    segment : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the segment.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of closest point.

    Examples
    --------
    >>>

    """
    a, b = segment
    p = closest_point_on_line(point, segment)
    d = distance_point_point_sqrd(a, b)
    d1 = distance_point_point_sqrd(a, p)
    d2 = distance_point_point_sqrd(b, p)
    if d1 > d or d2 > d:
        if d1 < d2:
            return a
        return b
    return p


def closest_point_on_segment_xy(point, segment):
    """Compute closest point on a line segment to a given point lying in the XY-plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point.
    segment : [point, point] | :class:`~compas.geometry.Line`
        Two 2D or 3D points defining the line segment (Z components will be ignored).

    Returns
    -------
    [float, float, 0.0]
        XYZ coordinates of closest point in the XY plane.

    """
    a, b = segment
    p = closest_point_on_line_xy(point, segment)
    d = distance_point_point_sqrd_xy(a, b)
    d1 = distance_point_point_sqrd_xy(a, p)
    d2 = distance_point_point_sqrd_xy(b, p)
    if d1 > d or d2 > d:
        if d1 < d2:
            return [a[0], a[1], 0.0]
        return [b[0], b[1], 0.0]
    return p


def closest_point_on_polyline(point, polyline):
    """Find the closest point on a polyline to a given point.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of a 2D or 3D point (Z will be ignored).
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        A sequence of XYZ coordinates representing the locations of the corners of a polyline.
        The vertices are assumed to be in order.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of closest point.

    """
    cloud = []

    for segment in pairwise(polyline):
        cloud.append(closest_point_on_segment(point, segment))

    return closest_point_in_cloud(point, cloud)[1]


def closest_point_on_polyline_xy(point, polyline):
    """Compute closest point on a polyline to a given point,
    assuming they both lie in the XY-plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        A sequence of XY(Z) coordinates of 2D or 3D points (Z will be ignored)
        representing the locations of the corners of a polyline.
        The vertices are assumed to be in order.

    Returns
    -------
    [float, float, 0.0]
        XYZ coordinates of closest point in the XY plane.

    """
    cloud = []

    for segment in pairwise(polyline):
        cloud.append(closest_point_on_segment_xy(point, segment))

    return closest_point_in_cloud_xy(point, cloud)[1]


def closest_point_on_polygon_xy(point, polygon):
    """Compute closest point on a polygon to a given point lying in the XY-plane.

    Parameters
    ----------
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a 2D or 3D point (Z will be ignored).
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        A sequence of XY(Z) coordinates of 2D or 3D points
        (Z will be ignored) representing the locations of the corners of a polygon.
        The vertices are assumed to be in order. The polygon is assumed to be closed:
        the first and last vertex in the sequence should not be the same.

    Returns
    -------
    [float, float, 0.0]
        XYZ coordinates of closest point in the XY plane.

    """
    points = []
    for i in range(len(polygon)):
        segment = polygon[i - 1], polygon[i]
        points.append(closest_point_on_segment_xy(point, segment))

    return closest_point_in_cloud_xy(point, points)[1]


def closest_point_on_plane(point, plane):
    """Compute closest point on a plane to a given point.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of point.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal defining the plane.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the closest point.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wikipedia. *Distance from a point to a plane*.
           Available at: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_plane

    Examples
    --------
    >>> plane = ([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])
    >>> point = [1.0, 2.0, 3.0]
    >>> closest_point_on_plane(point, plane)
    [1.0, 2.0, 0.0]

    """
    base, normal = plane
    x, y, z = base
    a, b, c = normalize_vector(normal)
    x1, y1, z1 = point
    d = a * x + b * y + c * z
    k = (a * x1 + b * y1 + c * z1 - d) / (a**2 + b**2 + c**2)
    return [x1 - k * a, y1 - k * b, z1 - k * c]


def closest_line_to_point(point, lines):
    """Compute closest line to a point from a list of lines.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of point.
    lines : sequence[[point, point] | :class:`~compas.geometry.Line`].
        The lines to be checked for distance.

    Returns
    -------
    tuple[point, point]
        The closest line.
    """
    cloud = []

    for segment in lines:
        cloud.append(closest_point_on_segment(point, segment))

    return lines[closest_point_in_cloud(point, cloud)[2]]


# src/compas/geometry/_core/kdtree.py

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

import collections

from .distance import distance_point_point_sqrd


Node = collections.namedtuple("Node", "point axis label left right")


class KDTree(object):
    """A tree for nearest neighbor search in a k-dimensional space.

    Parameters
    ----------
    objects : sequence[[float, float, float] | :class:`~compas.geometry.Point`], optional
        A list of objects to populate the tree with.
        If objects are provided, the tree is built automatically.
        Otherwise, use :meth:`build`.

    Attributes
    ----------
    root : Node
        The root node of the built tree.
        This is the median with respect to the different dimensions of the tree.

    Notes
    -----
    For more info, see [1]_ and [2]_.

    References
    ----------
    .. [1] Wikipedia. *k-d tree*.
           Available at: https://en.wikipedia.org/wiki/K-d_tree.
    .. [2] Dell'Amico, M. *KD-Tree for nearest neighbor search in a K-dimensional space (Python recipe)*.
           Available at: http://code.activestate.com/recipes/577497-kd-tree-for-nearest-neighbor-search-in-a-k-dimensional-space/.

    Examples
    --------
    >>>

    """

    def __init__(self, objects=None):
        self.root = None
        if objects:
            self.root = self.build([(o, i) for i, o in enumerate(objects)])

    def build(self, objects, axis=0):
        """Populate a kd-tree with given objects.

        Parameters
        ----------
        objects : sequence[tuple[[float, float, float] | :class:`~compas.geometry.Point`, int or str]]
            The tree objects as a sequence of point-label tuples.
        axis : int, optional
            The axis along which to build.

        Returns
        -------
        Node or None
            The root node, or None if the sequence of objects is empty.

        """
        if not objects:
            return

        objects.sort(key=lambda o: o[0][axis])
        median_idx = len(objects) // 2
        median_point, median_label = objects[median_idx]
        next_axis = (axis + 1) % 3

        return Node(
            median_point,
            axis,
            median_label,
            self.build(objects[:median_idx], next_axis),
            self.build(objects[median_idx + 1 :], next_axis),
        )

    def nearest_neighbor(self, point, exclude=None):
        """Find the nearest neighbor to a given point,
        excluding neighbors that have already been found.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            XYZ coordinates of the base point.
        exclude : sequence[int or str], optional
            A sequence of point identified by their label to exclude from the search.

        Returns
        -------
        [[float, float, float], int or str, float]
            XYZ coordinates of the nearest neighbor.
            Label of the nearest neighbor.
            Distance to the base point.

        """

        def search(node):
            if node is None:
                return

            d2 = distance_point_point_sqrd(point, node.point)
            if d2 < best[2]:
                if node.label not in exclude:
                    best[:] = node.point, node.label, d2

            d = point[node.axis] - node.point[node.axis]
            if d <= 0:
                close, far = node.left, node.right
            else:
                close, far = node.right, node.left

            search(close)
            if d**2 < best[2]:
                search(far)

        exclude = set(exclude or [])
        best = [None, None, float("inf")]
        search(self.root)
        best[2] **= 0.5
        return best

    def nearest_neighbors(self, point, number, distance_sort=False):
        """Find the N nearest neighbors to a given point.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            XYZ coordinates of the base point.
        number : int
            The number of nearest neighbors.
        distance_sort : bool, optional
            Sort the nearest neighbors by distance to the base point.

        Returns
        -------
        list[[[float, float, float], int or str, float]]
            A list of N nearest neighbors.

        """
        nnbrs = []
        exclude = set()
        for i in range(number):
            nnbr = self.nearest_neighbor(point, exclude)
            nnbrs.append(nnbr)
            exclude.add(nnbr[1])
        if distance_sort:
            return sorted(nnbrs, key=lambda nnbr: nnbr[2])
        return nnbrs


# src/compas/geometry/_core/matrices.py

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

import math

from copy import deepcopy

from ._algebra import scale_vector
from ._algebra import normalize_vector
from ._algebra import subtract_vectors
from ._algebra import cross_vectors
from ._algebra import dot_vectors
from ._algebra import multiply_matrix_vector
from ._algebra import length_vector
from ._algebra import allclose
from ._algebra import multiply_matrices
from ._algebra import transpose_matrix
from ._algebra import norm_vector


_EPS = 1e-16
"""eps for testing whether a number is close to zero"""

_SPEC2TUPLE = {
    "sxyz": (0, 0, 0, 0),
    "sxyx": (0, 0, 1, 0),
    "sxzy": (0, 1, 0, 0),
    "sxzx": (0, 1, 1, 0),
    "syzx": (1, 0, 0, 0),
    "syzy": (1, 0, 1, 0),
    "syxz": (1, 1, 0, 0),
    "syxy": (1, 1, 1, 0),
    "szxy": (2, 0, 0, 0),
    "szxz": (2, 0, 1, 0),
    "szyx": (2, 1, 0, 0),
    "szyz": (2, 1, 1, 0),
    "rzyx": (0, 0, 0, 1),
    "rxyx": (0, 0, 1, 1),
    "ryzx": (0, 1, 0, 1),
    "rxzx": (0, 1, 1, 1),
    "rxzy": (1, 0, 0, 1),
    "ryzy": (1, 0, 1, 1),
    "rzxy": (1, 1, 0, 1),
    "ryxy": (1, 1, 1, 1),
    "ryxz": (2, 0, 0, 1),
    "rzxz": (2, 0, 1, 1),
    "rxyz": (2, 1, 0, 1),
    "rzyz": (2, 1, 1, 1),
}
"""used for Euler angles: to map rotation type and axes to tuples of inner axis, parity, repetition, frame"""

_NEXT_SPEC = [1, 2, 0, 1]


def is_matrix_square(M):
    """Verify that a matrix is square.

    Parameters
    ----------
    M : list[list[float]]
        The matrix.

    Returns
    -------
    bool
        True if the length of every row is equal to the number of rows.
        False otherwise.

    Examples
    --------
    >>> M = identity_matrix(4)
    >>> is_matrix_square(M)
    True

    """
    number_of_rows = len(M)
    for row in M:
        if len(row) != number_of_rows:
            return False
    return True


def matrix_minor(M, i, j):
    """Construct the minor corresponding to an element of a matrix.

    Parameters
    ----------
    M : list[list[float]]
        The matrix.
    i : int
        Row index of the minor.
    j : int
        Column index of the minor.

    Returns
    -------
    list[list[float]]
        The minor.

    """
    return [row[:j] + row[j + 1 :] for row in (M[:i] + M[i + 1 :])]


def matrix_determinant(M, check=True):
    """Calculates the determinant of a square matrix M.

    Parameters
    ----------
    M : list[list[float]]
        A square matrix of any dimension.
    check : bool
        If True, checks if the matrix is square.

    Raises
    ------
    ValueError
        If the matrix is not square.

    Returns
    -------
    float
        The determinant.

    Examples
    --------
    >>> M = identity_matrix(4)
    >>> matrix_determinant(M)
    1.0

    """
    dim = len(M)

    if check:
        if not is_matrix_square(M):
            raise ValueError("Not a square matrix")

    if dim == 2:
        return M[0][0] * M[1][1] - M[0][1] * M[1][0]

    D = 0
    for c in range(dim):
        D += (-1) ** c * M[0][c] * matrix_determinant(matrix_minor(M, 0, c), check=False)
    return D


def matrix_inverse(M):
    """Calculates the inverse of a square matrix M.

    Parameters
    ----------
    M : list[list[float]]
        A square matrix of any dimension.

    Returns
    -------
    list[list[float]]
        The inverted matrix.

    Raises
    ------
    ValueError
        If the matrix is not squared
    ValueError
        If the matrix is singular.
    ValueError
        If the matrix is not invertible.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> f = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    >>> T = matrix_from_frame(f)
    >>> I = multiply_matrices(T, matrix_inverse(T))
    >>> I2 = identity_matrix(4)
    >>> allclose(I[0], I2[0])
    True
    >>> allclose(I[1], I2[1])
    True
    >>> allclose(I[2], I2[2])
    True
    >>> allclose(I[3], I2[3])
    True

    """
    D = matrix_determinant(M)

    if D == 0:
        ValueError("The matrix is singular.")

    if len(M) == 2:
        return [[M[1][1] / D, -1 * M[0][1] / D], [-1 * M[1][0] / D, M[0][0] / D]]

    cofactors = []
    for r in range(len(M)):
        cofactor_row = []
        for c in range(len(M)):
            cofactor_row.append((-1) ** (r + c) * matrix_determinant(matrix_minor(M, r, c)))
        cofactors.append(cofactor_row)

    cofactors = transpose_matrix(cofactors)

    for r in range(len(cofactors)):
        for c in range(len(cofactors)):
            cofactors[r][c] = cofactors[r][c] / D

    return cofactors


def decompose_matrix(M):
    """Calculates the components of rotation, translation, scale, shear, and
    perspective of a given transformation matrix M. [1]_

    Parameters
    ----------
    M : list[list[float]]
        The square matrix of any dimension.

    Raises
    ------
    ValueError
        If matrix is singular or degenerative.

    Returns
    -------
    scale : [float, float, float]
        The 3 scale factors in x-, y-, and z-direction.
    shear : [float, float, float]
        The 3 shear factors for x-y, x-z, and y-z axes.
    angles : [float, float, float]
        The rotation specified through the 3 Euler angles about static x, y, z axes.
    translation : [float, float, float]
        The 3 values of translation.
    perspective : [float, float, float, float]
        The 4 perspective entries of the matrix.

    Examples
    --------
    >>> trans1 = [1, 2, 3]
    >>> angle1 = [-2.142, 1.141, -0.142]
    >>> scale1 = [0.123, 2, 0.5]
    >>> T = matrix_from_translation(trans1)
    >>> R = matrix_from_euler_angles(angle1)
    >>> S = matrix_from_scale_factors(scale1)
    >>> M = multiply_matrices(multiply_matrices(T, R), S)
    >>> # M = compose_matrix(scale1, None, angle1, trans1, None)
    >>> scale2, shear2, angle2, trans2, persp2 = decompose_matrix(M)
    >>> allclose(scale1, scale2)
    True
    >>> allclose(angle1, angle2)
    True
    >>> allclose(trans1, trans2)
    True

    References
    ----------
    .. [1] Slabaugh, 1999. *Computing Euler angles from a rotation matrix*.
           Available at: http://www.gregslabaugh.net/publications/euler.pdf

    """
    fabs = math.fabs
    cos = math.cos
    atan2 = math.atan2
    asin = math.asin
    pi = math.pi

    detM = matrix_determinant(M)  # raises ValueError if matrix is not squared
    if detM == 0:
        ValueError("The matrix is singular.")

    Mt = transpose_matrix(M)
    if abs(Mt[3][3]) < _EPS:
        raise ValueError("The element [3,3] of the matrix is zero.")

    for i in range(4):
        for j in range(4):
            Mt[i][j] /= Mt[3][3]

    translation = [M[0][3], M[1][3], M[2][3]]

    # scale, shear, angles
    scale = [0.0, 0.0, 0.0]
    shear = [0.0, 0.0, 0.0]
    angles = [0.0, 0.0, 0.0]

    # copy Mt[:3, :3] into row
    row = [
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
    ]
    for i in range(3):
        for j in range(3):
            row[i][j] = Mt[i][j]

    scale[0] = norm_vector(row[0])
    for i in range(3):
        row[0][i] /= scale[0]  # type: ignore

    shear[0] = dot_vectors(row[0], row[1])
    for i in range(3):
        row[1][i] -= row[0][i] * shear[0]

    scale[1] = norm_vector(row[1])
    for i in range(3):
        row[1][i] /= scale[1]  # type: ignore

    shear[1] = dot_vectors(row[0], row[2])
    for i in range(3):
        row[2][i] -= row[0][i] * shear[1]

    # why is the order different here?
    # it certainly influences the result

    shear[2] = dot_vectors(row[1], row[2])
    for i in range(3):
        row[2][i] -= row[0][i] * shear[2]

    scale[2] = norm_vector(row[2])
    for i in range(3):
        row[2][i] /= scale[2]  # type: ignore

    shear[0] /= scale[1]
    shear[1] /= scale[2]
    shear[2] /= scale[2]

    if dot_vectors(row[0], cross_vectors(row[1], row[2])) < 0:
        scale = [-x for x in scale]
        row = [[-x for x in y] for y in row]

    # angles
    if row[0][2] != -1.0 and row[0][2] != 1.0:
        beta1 = asin(-row[0][2])
        # beta2 = pi - beta1
        alpha1 = atan2(row[1][2] / cos(beta1), row[2][2] / cos(beta1))
        # alpha2 = atan2(row[1][2] / cos(beta2), row[2][2] / cos(beta2))
        gamma1 = atan2(row[0][1] / cos(beta1), row[0][0] / cos(beta1))
        # gamma2 = atan2(row[0][1] / cos(beta2), row[0][0] / cos(beta2))
        angles = [alpha1, beta1, gamma1]

    else:
        gamma = 0.0
        if row[0][2] == -1.0:
            beta = pi / 2.0
            alpha = gamma + atan2(row[1][0], row[2][0])
        else:  # row[0][2] == 1
            beta = -pi / 2.0
            alpha = -gamma + atan2(-row[1][0], -row[2][0])
        angles = [alpha, beta, gamma]

    # perspective
    if fabs(Mt[0][3]) > _EPS and fabs(Mt[1][3]) > _EPS and fabs(Mt[2][3]) > _EPS:
        P = deepcopy(Mt)
        P[0][3], P[1][3], P[2][3], P[3][3] = 0.0, 0.0, 0.0, 1.0
        Ptinv = matrix_inverse(transpose_matrix(P))
        perspective = multiply_matrix_vector(Ptinv, [Mt[0][3], Mt[1][3], Mt[2][3], Mt[3][3]])
    else:
        perspective = [0.0, 0.0, 0.0, 1.0]

    return scale, shear, angles, translation, perspective


def compose_matrix(scale=None, shear=None, angles=None, translation=None, perspective=None):
    """Calculates a matrix from the components of scale, shear, euler_angles, translation and perspective.

    Parameters
    ----------
    scale : [float, float, float]
        The 3 scale factors in x-, y-, and z-direction.
    shear : [float, float, float]
        The 3 shear factors for x-y, x-z, and y-z axes.
    angles : [float, float, float]
        The rotation specified through the 3 Euler angles about static x, y, z axes.
    translation : [float, float, float]
        The 3 values of translation.
    perspective : [float, float, float, float]
        The 4 perspective entries of the matrix.

    Returns
    -------
    list[list[float]]
        The 4x4 matrix that combines the provided transformation components.

    Examples
    --------
    >>> trans1 = [1, 2, 3]
    >>> angle1 = [-2.142, 1.141, -0.142]
    >>> scale1 = [0.123, 2, 0.5]
    >>> M = compose_matrix(scale1, None, angle1, trans1, None)
    >>> scale2, shear2, angle2, trans2, persp2 = decompose_matrix(M)
    >>> allclose(scale1, scale2)
    True
    >>> allclose(angle1, angle2)
    True
    >>> allclose(trans1, trans2)
    True

    """
    M = [[1.0 if i == j else 0.0 for i in range(4)] for j in range(4)]
    if perspective is not None:
        P = matrix_from_perspective_entries(perspective)
        M = multiply_matrices(M, P)
    if translation is not None:
        T = matrix_from_translation(translation)
        M = multiply_matrices(M, T)
    if angles is not None:
        R = matrix_from_euler_angles(angles, static=True, axes="xyz")
        M = multiply_matrices(M, R)
    if shear is not None:
        H = matrix_from_shear_entries(shear)
        M = multiply_matrices(M, H)
    if scale is not None:
        S = matrix_from_scale_factors(scale)
        M = multiply_matrices(M, S)
    for i in range(4):
        for j in range(4):
            M[i][j] /= M[3][3]  # type: ignore
    return M


def identity_matrix(dim):
    """Construct an identity matrix.

    Parameters
    ----------
    dim : int
        The number of rows and/or columns of the matrix.

    Returns
    -------
    list of list
        A list of `dim` lists, with each list containing `dim` elements.
        The items on the "diagonal" are one.
        All other items are zero.

    Examples
    --------
    >>> identity_matrix(4)
    [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]

    """
    return [[1.0 if i == j else 0.0 for i in range(dim)] for j in range(dim)]


def matrix_from_frame(frame):
    """Computes a change of basis transformation from world XY to the frame.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`
        A frame describing the targeted Cartesian coordinate system

    Returns
    -------
    list[list[float]]
        A 4x4 transformation matrix representing the transformation from
        world coordinates to frame coordinates.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> f = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    >>> T = matrix_from_frame(f)

    """
    M = identity_matrix(4)
    M[0][0], M[1][0], M[2][0] = frame.xaxis
    M[0][1], M[1][1], M[2][1] = frame.yaxis
    M[0][2], M[1][2], M[2][2] = frame.zaxis
    M[0][3], M[1][3], M[2][3] = frame.point
    return M


def matrix_from_frame_to_frame(frame_from, frame_to):
    """Computes a transformation between two frames.

    This transformation allows to transform geometry from one Cartesian
    coordinate system defined by `frame_from` to another Cartesian
    coordinate system defined by `frame_to`.

    Parameters
    ----------
    frame_from : :class:`~compas.geometry.Frame`
        A frame defining the original Cartesian coordinate system
    frame_to : :class:`~compas.geometry.Frame`
        A frame defining the targeted Cartesian coordinate system

    Returns
    -------
    list[list[float]]
        A 4x4 transformation matrix representing the transformation
        from one frame to another.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> f1 = Frame([2, 2, 2], [0.12, 0.58, 0.81], [-0.80, 0.53, -0.26])
    >>> f2 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    >>> T = matrix_from_frame_to_frame(f1, f2)
    """
    T1 = matrix_from_frame(frame_from)
    T2 = matrix_from_frame(frame_to)
    return multiply_matrices(T2, matrix_inverse(T1))


def matrix_from_change_of_basis(frame_from, frame_to):
    """Computes a change of basis transformation between two frames.

    A basis change is essentially a remapping of geometry from one
    coordinate system to another.

    Parameters
    ----------
    frame_from : :class:`~compas.geometry.Frame`
        A frame defining the original Cartesian coordinate system
    frame_to : :class:`~compas.geometry.Frame`
        A frame defining the targeted Cartesian coordinate system

    Returns
    -------
    list[list[float]]
        A 4x4 transformation matrix representing a change of basis.

    Examples
    --------
    >>> from compas.geometry import Point, Frame
    >>> f1 = Frame([2, 2, 2], [0.12, 0.58, 0.81], [-0.80, 0.53, -0.26])
    >>> f2 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    >>> T = matrix_from_change_of_basis(f1, f2)

    """
    T1 = matrix_from_frame(frame_from)
    T2 = matrix_from_frame(frame_to)
    return multiply_matrices(matrix_inverse(T2), T1)


def matrix_from_euler_angles(euler_angles, static=True, axes="xyz"):
    """Calculates a rotation matrix from Euler angles.

    In 3D space any orientation can be achieved by composing three elemental
    rotations, rotations about the axes (x, y, z) of a coordinate system. A
    triple of Euler angles can be interpreted in 24 ways, which depends on if
    the rotations are applied to a static (extrinsic) or rotating (intrinsic)
    frame and the order of axes.

    Parameters
    ----------
    euler_angles : [float, float, float]
        Three numbers that represent the angles of rotations about the defined axes.
    static : bool, optional
        If True the rotations are applied to a static frame.
        If False, to a rotational.
    axes : Literal['xyz', 'yzx', 'zxy'], optional
        A 3 character string specifying order of the axes.

    Returns
    -------
    list[list[float]]
        A 4x4 transformation matrix representing a rotation.

    Examples
    --------
    >>> ea1 = 1.4, 0.5, 2.3
    >>> R = matrix_from_euler_angles(ea1)
    >>> ea2 = euler_angles_from_matrix(R)
    >>> allclose(ea1, ea2)
    True

    """
    global _SPEC2TUPLE
    global _NEXT_SPEC

    sin = math.sin
    cos = math.cos

    ai, aj, ak = euler_angles

    if static:
        firstaxis, parity, repetition, frame = _SPEC2TUPLE["s" + axes]
    else:
        firstaxis, parity, repetition, frame = _SPEC2TUPLE["r" + axes]

    i = firstaxis
    j = _NEXT_SPEC[i + parity]
    k = _NEXT_SPEC[i - parity + 1]

    if frame:
        ai, ak = ak, ai
    if parity:
        ai, aj, ak = -ai, -aj, -ak

    si, sj, sk = sin(ai), sin(aj), sin(ak)
    ci, cj, ck = cos(ai), cos(aj), cos(ak)
    cc, cs = ci * ck, ci * sk
    sc, ss = si * ck, si * sk

    M = [[1.0 if x == y else 0.0 for x in range(4)] for y in range(4)]

    if repetition:
        M[i][i] = cj
        M[i][j] = sj * si
        M[i][k] = sj * ci
        M[j][i] = sj * sk
        M[j][j] = -cj * ss + cc
        M[j][k] = -cj * cs - sc
        M[k][i] = -sj * ck
        M[k][j] = cj * sc + cs
        M[k][k] = cj * cc - ss
    else:
        M[i][i] = cj * ck
        M[i][j] = sj * sc - cs
        M[i][k] = sj * cc + ss
        M[j][i] = cj * sk
        M[j][j] = sj * ss + cc
        M[j][k] = sj * cs - sc
        M[k][i] = -sj
        M[k][j] = cj * si
        M[k][k] = cj * ci

    return M


def euler_angles_from_matrix(M, static=True, axes="xyz"):
    """Returns Euler angles from the rotation matrix M according to specified
    axis sequence and type of rotation.

    Parameters
    ----------
    M : list[list[float]]
        The 3x3 or 4x4 matrix in row-major order.
    static : bool, optional
        If True the rotations are applied to a static frame.
        If False, to a rotational.
    axes : str, optional
        A 3 character string specifying order of the axes.

    Returns
    -------
    list[float]
        The 3 Euler angles.

    Examples
    --------
    >>> ea1 = 1.4, 0.5, 2.3
    >>> R = matrix_from_euler_angles(ea1)
    >>> ea2 = euler_angles_from_matrix(R)
    >>> allclose(ea1, ea2)
    True

    """
    global _SPEC2TUPLE
    global _NEXT_SPEC
    global _EPS

    atan2 = math.atan2
    sqrt = math.sqrt

    if static:
        firstaxis, parity, repetition, frame = _SPEC2TUPLE["s" + axes]
    else:
        firstaxis, parity, repetition, frame = _SPEC2TUPLE["r" + axes]

    i = firstaxis
    j = _NEXT_SPEC[i + parity]
    k = _NEXT_SPEC[i - parity + 1]

    if repetition:
        sy = sqrt(M[i][j] * M[i][j] + M[i][k] * M[i][k])
        if sy > _EPS:
            ax = atan2(M[i][j], M[i][k])
            ay = atan2(sy, M[i][i])
            az = atan2(M[j][i], -M[k][i])
        else:
            ax = atan2(-M[j][k], M[j][j])
            ay = atan2(sy, M[i][i])
            az = 0.0
    else:
        cy = sqrt(M[i][i] * M[i][i] + M[j][i] * M[j][i])
        if cy > _EPS:
            ax = atan2(M[k][j], M[k][k])
            ay = atan2(-M[k][i], cy)
            az = atan2(M[j][i], M[i][i])
        else:
            ax = atan2(-M[j][k], M[j][j])
            ay = atan2(-M[k][i], cy)
            az = 0.0

    if parity:
        ax, ay, az = -ax, -ay, -az
    if frame:
        ax, az = az, ax

    return [ax, ay, az]


def matrix_from_axis_and_angle(axis, angle, point=None):
    """Calculates a rotation matrix from an rotation axis, an angle and an optional
    point of rotation.

    Parameters
    ----------
    axis : [float, float, float]
        Three numbers that represent the axis of rotation.
    angle : float
        The rotation angle in radians.
    point : [float, float, float] | :class:`~compas.geometry.Point`, optional
        A point to perform a rotation around an origin other than [0, 0, 0].

    Returns
    -------
    list[list[float]]
        A 4x4 transformation matrix representing a rotation.

    Notes
    -----
    The rotation is based on the right hand rule, i.e. anti-clockwise if the
    axis of rotation points towards the observer.

    Examples
    --------
    >>> axis1 = normalize_vector([-0.043, -0.254, 0.617])
    >>> angle1 = 0.1
    >>> R = matrix_from_axis_and_angle(axis1, angle1)
    >>> axis2, angle2 = axis_and_angle_from_matrix(R)
    >>> allclose(axis1, axis2)
    True
    >>> allclose([angle1], [angle2])
    True

    """
    if not point:
        point = [0.0, 0.0, 0.0]

    axis = list(axis)
    if length_vector(axis):
        axis = normalize_vector(axis)

    sina = math.sin(angle)
    cosa = math.cos(angle)

    R = [[cosa, 0.0, 0.0], [0.0, cosa, 0.0], [0.0, 0.0, cosa]]

    outer_product = [[axis[i] * axis[j] * (1.0 - cosa) for i in range(3)] for j in range(3)]
    R = [[R[i][j] + outer_product[i][j] for i in range(3)] for j in range(3)]

    axis = scale_vector(axis, sina)
    m = [[0.0, -axis[2], axis[1]], [axis[2], 0.0, -axis[0]], [-axis[1], axis[0], 0.0]]

    M = identity_matrix(4)

    for i in range(3):
        for j in range(3):
            R[i][j] += m[i][j]
            M[i][j] = R[i][j]

    # rotation about axis, angle AND point includes also translation
    t = subtract_vectors(point, multiply_matrix_vector(R, point))
    M[0][3] = t[0]
    M[1][3] = t[1]
    M[2][3] = t[2]

    return M


def matrix_from_axis_angle_vector(axis_angle_vector, point=[0, 0, 0]):
    """Calculates a rotation matrix from an axis-angle vector.

    Parameters
    ----------
    axis_angle_vector : [float, float, float]
        Three numbers that represent the axis of rotation and angle of rotation
        through the vector's magnitude.
    point : [float, float, float] | :class:`~compas.geometry.Point`, optional
        A point to perform a rotation around an origin other than [0, 0, 0].

    Returns
    -------
    list[list[float]]
        The 4x4 transformation matrix representing a rotation.

    Examples
    --------
    >>> aav1 = [-0.043, -0.254, 0.617]
    >>> R = matrix_from_axis_angle_vector(aav1)
    >>> aav2 = axis_angle_vector_from_matrix(R)
    >>> allclose(aav1, aav2)
    True

    """
    axis = list(axis_angle_vector)
    angle = length_vector(axis_angle_vector)
    return matrix_from_axis_and_angle(axis, angle, point)


def axis_and_angle_from_matrix(M):
    """Returns the axis and the angle of the rotation matrix M.

    Parameters
    ----------
    M : list[list[float]]
        The 4-by-4 transformation matrix.

    Returns
    -------
    [float, float, float]
        The rotation axis.
    float
        The rotation angle in radians.

    """
    fabs = math.fabs
    sqrt = math.sqrt

    eps = 0.01  # margin to allow for rounding errors
    eps2 = 0.1  # margin to distinguish between 0 and 180 degrees

    if all(fabs(M[i][j] - M[j][i]) < eps for i, j in [(0, 1), (0, 2), (1, 2)]):
        if (
            all(fabs(M[i][j] - M[j][i]) < eps2 for i, j in [(0, 1), (0, 2), (1, 2)])
            and fabs(M[0][0] + M[1][1] + M[2][2] - 3) < eps2
        ):
            return [0, 0, 0], 0

        angle = math.pi
        xx = (M[0][0] + 1) / 2
        yy = (M[1][1] + 1) / 2
        zz = (M[2][2] + 1) / 2
        xy = (M[0][1] + M[1][0]) / 4
        xz = (M[0][2] + M[2][0]) / 4
        yz = (M[1][2] + M[2][1]) / 4
        root_half = sqrt(0.5)
        if (xx > yy) and (xx > zz):
            if xx < eps:
                axis = [0, root_half, root_half]
            else:
                x = sqrt(xx)
                axis = [x, xy / x, xz / x]
        elif yy > zz:
            if yy < eps:
                axis = [root_half, 0, root_half]
            else:
                y = sqrt(yy)
                axis = [xy / y, y, yz / y]
        else:
            if zz < eps:
                axis = [root_half, root_half, 0]
            else:
                z = sqrt(zz)
                axis = [xz / z, yz / z, z]

        return axis, angle

    s = sqrt(
        (M[2][1] - M[1][2]) * (M[2][1] - M[1][2])
        + (M[0][2] - M[2][0]) * (M[0][2] - M[2][0])
        + (M[1][0] - M[0][1]) * (M[1][0] - M[0][1])
    )

    # should this also be an eps?
    if fabs(s) < 0.001:
        s = 1

    angle = math.acos((M[0][0] + M[1][1] + M[2][2] - 1) / 2)

    x = (M[2][1] - M[1][2]) / s
    y = (M[0][2] - M[2][0]) / s
    z = (M[1][0] - M[0][1]) / s

    return [x, y, z], angle


def axis_angle_vector_from_matrix(M):
    """Returns the axis-angle vector of the rotation matrix M.

    Parameters
    ----------
    M : list[list[float]]
        The 4-by-4 transformation matrix.

    Returns
    -------
    [float, float, float]
        The axis-angle vector.

    """
    axis, angle = axis_and_angle_from_matrix(M)
    return scale_vector(axis, angle)


def matrix_from_quaternion(quaternion):
    """Calculates a rotation matrix from quaternion coefficients.

    Parameters
    ----------
    quaternion : [float, float, float, float]
        Four numbers that represents the four coefficient values of a quaternion.

    Returns
    -------
    list[list[float]]
        The 4x4 transformation matrix representing a rotation.

    Raises
    ------
    ValueError
        If quaternion is invalid.

    Examples
    --------
    >>> q1 = [0.945, -0.021, -0.125, 0.303]
    >>> R = matrix_from_quaternion(q1)
    >>> q2 = quaternion_from_matrix(R)
    >>> allclose(q1, q2, tol=1e-03)
    True

    """
    sqrt = math.sqrt

    q = quaternion
    n = q[0] ** 2 + q[1] ** 2 + q[2] ** 2 + q[3] ** 2  # dot product

    # perhaps this should not be hard-coded?
    eps = 1.0e-15

    if n < eps:
        raise ValueError("Invalid quaternion, dot product must be != 0.")

    q = [v * sqrt(2.0 / n) for v in q]
    q = [[q[i] * q[j] for i in range(4)] for j in range(4)]  # outer_product

    rotation = [
        [1.0 - q[2][2] - q[3][3], q[1][2] - q[3][0], q[1][3] + q[2][0], 0.0],
        [q[1][2] + q[3][0], 1.0 - q[1][1] - q[3][3], q[2][3] - q[1][0], 0.0],
        [q[1][3] - q[2][0], q[2][3] + q[1][0], 1.0 - q[1][1] - q[2][2], 0.0],
        [0.0, 0.0, 0.0, 1.0],
    ]
    return rotation


def quaternion_from_matrix(M):
    """Returns the 4 quaternion coefficients from a rotation matrix.

    Parameters
    ----------
    M : list[list[float]]
        The coefficients of the rotation matrix, row per row.

    Returns
    -------
    [float, float, float, float]
        The quaternion coefficients.

    Examples
    --------
    >>> q1 = [0.945, -0.021, -0.125, 0.303]
    >>> R = matrix_from_quaternion(q1)
    >>> q2 = quaternion_from_matrix(R)
    >>> allclose(q1, q2, tol=1e-03)
    True

    """
    sqrt = math.sqrt

    qw, qx, qy, qz = 0, 0, 0, 0
    trace = M[0][0] + M[1][1] + M[2][2]

    if trace > 0.0:
        s = 0.5 / sqrt(trace + 1.0)
        qw = 0.25 / s
        qx = (M[2][1] - M[1][2]) * s
        qy = (M[0][2] - M[2][0]) * s
        qz = (M[1][0] - M[0][1]) * s

    elif (M[0][0] > M[1][1]) and (M[0][0] > M[2][2]):
        s = 2.0 * sqrt(1.0 + M[0][0] - M[1][1] - M[2][2])
        qw = (M[2][1] - M[1][2]) / s
        qx = 0.25 * s
        qy = (M[0][1] + M[1][0]) / s
        qz = (M[0][2] + M[2][0]) / s

    elif M[1][1] > M[2][2]:
        s = 2.0 * sqrt(1.0 + M[1][1] - M[0][0] - M[2][2])
        qw = (M[0][2] - M[2][0]) / s
        qx = (M[0][1] + M[1][0]) / s
        qy = 0.25 * s
        qz = (M[1][2] + M[2][1]) / s
    else:
        s = 2.0 * sqrt(1.0 + M[2][2] - M[0][0] - M[1][1])
        qw = (M[1][0] - M[0][1]) / s
        qx = (M[0][2] + M[2][0]) / s
        qy = (M[1][2] + M[2][1]) / s
        qz = 0.25 * s

    return [qw, qx, qy, qz]


def matrix_from_basis_vectors(xaxis, yaxis):
    """Creates a rotation matrix from basis vectors (= orthonormal vectors).

    Parameters
    ----------
    xaxis : [float, float, float] | :class:`~compas.geometry.Vector`
        The x-axis of the frame.
    yaxis : [float, float, float] | :class:`~compas.geometry.Vector`
        The y-axis of the frame.

    Returns
    -------
    list[list[float]]
        A 4x4 transformation matrix representing a rotation.

    Notes
    -----
    .. code-block:: none

        [ x0  y0  z0  0 ]
        [ x1  y1  z1  0 ]
        [ x2  y2  z2  0 ]
        [  0   0   0  1 ]

    Examples
    --------
    >>> xaxis = [0.68, 0.68, 0.27]
    >>> yaxis = [-0.67, 0.73, -0.15]
    >>> R = matrix_from_basis_vectors(xaxis, yaxis)

    """
    xaxis = normalize_vector(list(xaxis))
    yaxis = normalize_vector(list(yaxis))
    zaxis = cross_vectors(xaxis, yaxis)
    yaxis = cross_vectors(zaxis, xaxis)

    R = identity_matrix(4)
    R[0][0], R[1][0], R[2][0] = xaxis
    R[0][1], R[1][1], R[2][1] = yaxis
    R[0][2], R[1][2], R[2][2] = zaxis
    return R


def basis_vectors_from_matrix(R):
    """Returns the basis vectors from the rotation matrix R.

    Parameters
    ----------
    R : list[list[float]]
        A 4-by-4 transformation matrix, or a 3-by-3 rotation matrix.

    Returns
    -------
    [float, float, float]
        The first basis vector of the rotation.
    [float, float, float]
        The second basis vector of the rotation.

    Raises
    ------
    ValueError
        If rotation matrix is invalid.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> f = Frame([0, 0, 0], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    >>> R = matrix_from_frame(f)
    >>> xaxis, yaxis = basis_vectors_from_matrix(R)

    """
    xaxis = [R[0][0], R[1][0], R[2][0]]
    yaxis = [R[0][1], R[1][1], R[2][1]]
    zaxis = [R[0][2], R[1][2], R[2][2]]

    if not allclose(zaxis, cross_vectors(xaxis, yaxis)):
        raise ValueError("Matrix is invalid rotation matrix.")

    return xaxis, yaxis


def matrix_from_translation(translation):
    """Returns a 4x4 translation matrix in row-major order.

    Parameters
    ----------
    translation : [float, float, float]
        The x, y and z components of the translation.

    Returns
    -------
    list[list[float]]
        The 4x4 transformation matrix representing a translation.

    Notes
    -----
    .. code-block:: none

        [ .  .  .  0 ]
        [ .  .  .  1 ]
        [ .  .  .  2 ]
        [ .  .  .  . ]

    Examples
    --------
    >>> T = matrix_from_translation([1, 2, 3])

    """
    M = identity_matrix(4)
    M[0][3] = float(translation[0])
    M[1][3] = float(translation[1])
    M[2][3] = float(translation[2])

    return M


def translation_from_matrix(M):
    """Returns the 3 values of translation from the matrix M.

    Parameters
    ----------
    M : list[list[float]]
        A 4-by-4 transformation matrix.

    Returns
    -------
    [float, float, float]
        The translation vector.

    """
    return [M[0][3], M[1][3], M[2][3]]


def matrix_from_orthogonal_projection(plane):
    """Returns an orthogonal projection matrix to project onto a plane.

    Parameters
    ----------
    plane : [point, normal] | :class:`~compas.geometry.Plane`
        The plane to project onto.

    Returns
    -------
    list[list[float]]
        The 4x4 transformation matrix representing an orthogonal projection.

    Examples
    --------
    >>> point = [0, 0, 0]
    >>> normal = [0, 0, 1]
    >>> plane = (point, normal)
    >>> P = matrix_from_orthogonal_projection(plane)

    """
    point, normal = plane
    T = identity_matrix(4)
    normal = normalize_vector(normal)

    for j in range(3):
        for i in range(3):
            T[i][j] -= normal[i] * normal[j]  # outer_product

    T[0][3], T[1][3], T[2][3] = scale_vector(normal, dot_vectors(point, normal))
    return T


def matrix_from_parallel_projection(plane, direction):
    """Returns an parallel projection matrix to project onto a plane.

    Parameters
    ----------
    plane : [point, normal] | :class:`~compas.geometry.Plane`
        The plane to project onto.
    direction : [float, float, float] | :class:`~compas.geometry.Vector`
        Direction of the projection.

    Returns
    -------
    list[list[float]]
        A 4-by-4 transformation matrix.

    Examples
    --------
    >>> point = [0, 0, 0]
    >>> normal = [0, 0, 1]
    >>> plane = (point, normal)
    >>> direction = [1, 1, 1]
    >>> P = matrix_from_parallel_projection(plane, direction)

    """
    point, normal = plane
    T = identity_matrix(4)
    normal = normalize_vector(normal)

    scale = dot_vectors(direction, normal)
    for j in range(3):
        for i in range(3):
            T[i][j] -= direction[i] * normal[j] / scale

    T[0][3], T[1][3], T[2][3] = scale_vector(direction, dot_vectors(point, normal) / scale)
    return T


def matrix_from_perspective_projection(plane, center_of_projection):
    """Returns a perspective projection matrix to project onto a plane along lines that emanate from a single point, called the center of projection.

    Parameters
    ----------
    plane : [point, normal] | :class:`~compas.geometry.Plane`
        The plane to project onto.
    center_of_projection : [float, float, float] | :class:`~compas.geometry.Point`
        The camera view point.

    Returns
    -------
    list[list[float]]
        A 4-by-4 transformation matrix.

    Examples
    --------
    >>> point = [0, 0, 0]
    >>> normal = [0, 0, 1]
    >>> plane = (point, normal)
    >>> center_of_projection = [1, 1, 0]
    >>> P = matrix_from_perspective_projection(plane, center_of_projection)

    """
    point, normal = plane
    T = identity_matrix(4)
    normal = normalize_vector(normal)

    T[0][0] = T[1][1] = T[2][2] = dot_vectors(subtract_vectors(center_of_projection, point), normal)

    for j in range(3):
        for i in range(3):
            T[i][j] -= center_of_projection[i] * normal[j]

    T[0][3], T[1][3], T[2][3] = scale_vector(center_of_projection, dot_vectors(point, normal))

    for i in range(3):
        T[3][i] -= normal[i]

    T[3][3] = dot_vectors(center_of_projection, normal)

    return T


def matrix_from_perspective_entries(perspective):
    """Returns a matrix from perspective entries.

    Parameters
    ----------
    values : [float, float, float, float]
        The 4 perspective entries of a matrix.

    Returns
    -------
    list[list[float]]
        A 4-by-4 transformation matrix.

    Notes
    -----
    .. code-block:: none

        [ .  .  .  . ]
        [ .  .  .  . ]
        [ .  .  .  . ]
        [ 0  1  2  3 ]

    """
    M = identity_matrix(4)
    M[3][0] = float(perspective[0])
    M[3][1] = float(perspective[1])
    M[3][2] = float(perspective[2])
    M[3][3] = float(perspective[3])
    return M


def matrix_from_shear_entries(shear_entries):
    """Returns a shear matrix from the 3 factors for x-y, x-z, and y-z axes.

    Parameters
    ----------
    shear_entries : [float, float, float]
        The 3 shear factors for x-y, x-z, and y-z axes.

    Returns
    -------
    list[list[float]]
        A 4-by-4 transformation matrix.

    Notes
    -----
    .. code-block:: none

        [ .  0  1  . ]
        [ .  .  2  . ]
        [ .  .  .  . ]
        [ .  .  .  . ]

    Examples
    --------
    >>> Sh = matrix_from_shear_entries([1, 2, 3])

    """
    M = identity_matrix(4)
    M[0][1] = float(shear_entries[0])
    M[0][2] = float(shear_entries[1])
    M[1][2] = float(shear_entries[2])
    return M


def matrix_from_shear(angle, direction, point, normal):
    """Constructs a shear matrix by an angle along the direction vector on the
    shear plane (defined by point and normal).

    Parameters
    ----------
    angle : float
        The angle in radians.
    direction : [float, float, float] | :class:`~compas.geometry.Vector`
        The direction vector as list of 3 numbers.
        It must be orthogonal to the normal vector.
    point : [float, float, float] | :class:`~compas.geometry.Point`
        The point of the shear plane as list of 3 numbers.
    normal : [float, float, float] | :class:`~compas.geometry.Vector`
        The normal of the shear plane as list of 3 numbers.

    Returns
    -------
    list[list[float]]
        A 4-by-4 transformation matrix.

    Raises
    ------
    ValueError
        If direction and normal are not orthogonal.

    Notes
    -----
    A point P is transformed by the shear matrix into P" such that
    the vector P-P" is parallel to the direction vector and its extent is
    given by the angle of P-P'-P", where P' is the orthogonal projection
    of P onto the shear plane (defined by point and normal).

    Examples
    --------
    >>> angle = 0.1
    >>> direction = [0.1, 0.2, 0.3]
    >>> point = [4, 3, 1]
    >>> normal = cross_vectors(direction, [1, 0.3, -0.1])
    >>> S = matrix_from_shear(angle, direction, point, normal)

    """
    fabs = math.fabs

    normal = normalize_vector(normal)
    direction = normalize_vector(direction)

    if fabs(dot_vectors(normal, direction)) > _EPS:
        raise ValueError("Direction and normal vectors are not orthogonal")

    angle = math.tan(angle)
    M = identity_matrix(4)

    for j in range(3):
        for i in range(3):
            M[i][j] += angle * direction[i] * normal[j]

    M[0][3], M[1][3], M[2][3] = scale_vector(direction, -angle * dot_vectors(point, normal))

    return M


def matrix_from_scale_factors(scale_factors):
    """Returns a 4x4 scaling transformation.

    Parameters
    ----------
    scale_factors : [float, float, float]
        Three numbers defining the scaling factors in x, y, and z respectively.

    Returns
    -------
    list[list[float]]
        A 4-by-4 transformation matrix.

    Notes
    -----
    .. code-block:: python

        [ 0  .  .  . ]
        [ .  1  .  . ]
        [ .  .  2  . ]
        [ .  .  .  . ]

    Examples
    --------
    >>> Sc = matrix_from_scale_factors([1, 2, 3])

    """
    M = identity_matrix(4)
    M[0][0] = float(scale_factors[0])
    M[1][1] = float(scale_factors[1])
    M[2][2] = float(scale_factors[2])

    return M


def quaternion_from_euler_angles(e, static=True, axes="xyz"):
    """Returns a quaternion from Euler angles.

    Parameters
    ----------
    euler_angles : [float, float, float]
        Three numbers that represent the angles of rotations about the specified axes.
    static : bool, optional
        If True, the rotations are applied to a static frame.
        If False, the rotations are applied to a rotational frame.
    axes : str, optional
        A three-character string specifying the order of the axes.

    Returns
    -------
    [float, float, float, float]
        Quaternion as a list of four real values ``[w, x, y, z]``.

    """
    m = matrix_from_euler_angles(e, static, axes)
    q = quaternion_from_matrix(m)
    return q


def euler_angles_from_quaternion(q, static=True, axes="xyz"):
    """Returns Euler angles from a quaternion.

    Parameters
    ----------
    quaternion : [float, float, float, float]
        Quaternion as a list of four real values ``[w, x, y, z]``.
    static : bool, optional
        If True, the rotations are applied to a static frame.
        If False, the rotations are applied to a rotational frame.
    axes : str, optional
        A three-character string specifying the order of the axes.

    Returns
    -------
    [float, float, float]
        Euler angles as a list of three real values ``[a, b, c]``.

    """
    m = matrix_from_quaternion(q)
    e = euler_angles_from_matrix(m, static, axes)
    return e


def quaternion_from_axis_angle(axis, angle):
    """Returns a quaternion describing a rotation around the given axis by the given angle.

    Parameters
    ----------
    axis : [float, float, float] | :class:`~compas.geometry.Vector`
        XYZ coordinates of the rotation axis vector.
    angle : float
        Angle of rotation in radians.

    Returns
    -------
    [float, float, float, float]
        Quaternion as a list of four real values ``[qw, qx, qy, qz]``.

    Examples
    --------
    >>> axis =  [1.0, 0.0, 0.0]
    >>> angle = math.pi/2
    >>> q = quaternion_from_axis_angle(axis, angle)
    >>> allclose(q, [math.sqrt(2)/2, math.sqrt(2)/2, 0, 0])
    True

    """
    m = matrix_from_axis_and_angle(axis, angle, None)
    q = quaternion_from_matrix(m)
    return q


def axis_angle_from_quaternion(q):
    """Returns an axis and an angle of rotation from the given quaternion.

    Parameters
    ----------
    q : [float, float, float, float]
        Quaternion as a list of four real values ``[qw, qx, qy, qz]``.

    Returns
    -------
    axis : [float, float, float]
        XYZ coordinates of the rotation axis vector.
    angle : float
        Angle of rotation in radians.

    Examples
    --------
    >>> q = [1., 1., 0., 0.]
    >>> axis, angle = axis_angle_from_quaternion(q)
    >>> allclose(axis, [1., 0., 0.])
    True
    >>> allclose([angle], [math.pi/2], 1e-6)
    True

    """
    m = matrix_from_quaternion(q)
    axis, angle = axis_and_angle_from_matrix(m)
    return axis, angle


# src/compas/geometry/_core/normals.py

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

from ._algebra import subtract_vectors
from ._algebra import subtract_vectors_xy
from ._algebra import cross_vectors
from ._algebra import cross_vectors_xy
from ._algebra import length_vector
from ._algebra import normalize_vector

from .centroids import centroid_points


def normal_polygon(polygon, unitized=True):
    """Compute the normal of a polygon defined by a sequence of points.

    Parameters
    ----------
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        A list of polygon point coordinates.
    unitized : bool, optional
        If True, unitize the normal vector.

    Returns
    -------
    [float, float, float]
        The normal vector.

    Raises
    ------
    AssertionError
        If less than three points are provided.

    Notes
    -----
    The points in the list should be unique. For example, the first and last
    point in the list should not be the same.

    """
    p = len(polygon)

    assert p > 2, "At least three points required"

    nx = 0
    ny = 0
    nz = 0

    o = centroid_points(polygon)
    a = polygon[-1]
    oa = subtract_vectors(a, o)

    for i in range(p):
        b = polygon[i]
        ob = subtract_vectors(b, o)
        n = cross_vectors(oa, ob)
        oa = ob

        nx += n[0] * 0.5
        ny += n[1] * 0.5
        nz += n[2] * 0.5

    if not unitized:
        return [nx, ny, nz]

    return normalize_vector([nx, ny, nz])


def normal_triangle(triangle, unitized=True):
    """Compute the normal vector of a triangle.

    Parameters
    ----------
    triangle : [point, point, point] | :class:`~compas.geometry.Polygon`
        A list of triangle point coordinates.
    unitized : bool, optional
        If True, unitize the normal vector.

    Returns
    -------
    [float, float, float]
        The normal vector.

    Raises
    ------
    AssertionError
        If the triangle does not have three vertices.

    """
    assert len(triangle) == 3, "Three points are required."
    a, b, c = triangle
    ab = subtract_vectors(b, a)
    ac = subtract_vectors(c, a)
    n = cross_vectors(ab, ac)
    if not unitized:
        return n
    lvec = 1 / length_vector(n)
    return [lvec * n[0], lvec * n[1], lvec * n[2]]


def normal_triangle_xy(triangle, unitized=True):
    """Compute the normal vector of a triangle assumed to lie in the XY plane.

    Parameters
    ----------
    triangle : [point, point, point] | :class:`~compas.geometry.Polygon`
        A list of triangle point coordinates.
        Z-coordinates are ignored.
    unitized : bool, optional
        If True, unitize the normal vector.

    Returns
    -------
    [float, float, float]
        The normal vector, which is a vector perpendicular to the XY plane.

    Raises
    ------
    AssertionError
        If the triangle does not have three vertices.

    """
    a, b, c = triangle
    ab = subtract_vectors_xy(b, a)
    ac = subtract_vectors_xy(c, a)
    n = cross_vectors_xy(ab, ac)
    if not unitized:
        return n
    return [0, 0, n[2] / length_vector(n)]


# src/compas/geometry/_core/quaternions.py

"""
This module contains functions that operate on and/or return quaternions.

Notes
-----
The default convention to represent a quaternion :math:`q` in this module is by four real values **w**, **x**, **y**, **z**.
The first value **w** is the scalar (real) part, and **x**, **y**, **z** form the vector (complex, imaginary) part [1]_, so that:

:math:`q = w + xi + yj + zk`

where :math:`i, j, k` are basis components with following multiplication rules [2]_:

:math:`ii = jj = kk = ijk = -1`

:math:`ij = k,\\qquad ji = -k`

:math:`jk = i,\\qquad kj = -i`

:math:`ki = j,\\qquad ik = -j`

Quaternions are associative but not commutative.

**Quaternion as rotation.**
A rotation through an angle :math:`\\theta` around an axis defined by a euclidean unit vector :math:`u = u_{x}i + u_{y}j + u_{z}k`
can be represented as a quaternion:

:math:`q = cos(\\frac{\\theta}{2}) + sin(\\frac{\\theta}{2})  [u_{x}i + u_{y}j + u_{z}k]`

i.e.:

:math:`w = cos(\\frac{\\theta}{2})`

:math:`x = sin(\\frac{\\theta}{2})  u_{x}`

:math:`y = sin(\\frac{\\theta}{2})  u_{y}`

:math:`z = sin(\\frac{\\theta}{2})  u_{z}`

For a quaternion to represent a rotation or orientation, it must be unit-length.
A quaternion representing a rotation :math:`p` resulting from applying a rotation :math:`r` to a rotation :math:`q`, i.e.:
:math:`p = rq`,
is also unit-length.

References
----------
.. [1] http://mathworld.wolfram.com/Quaternion.html
.. [2] http://mathworld.wolfram.com/HamiltonsRules.html
.. [3] https://github.com/matthew-brett/transforms3d/blob/master/transforms3d/quaternions.py
"""
import math

from ._algebra import allclose


ATOL = 1e-6  # absolute tolerance


def quaternion_norm(q):
    """Calculates the length (euclidean norm) of a quaternion.

    Parameters
    ----------
    q : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
        Quaternion or sequence of four floats ``[w, x, y, z]``.

    Returns
    -------
    float
        The length (euclidean norm) of a quaternion.

    References
    ----------
    * Quaternion Norm: http://mathworld.wolfram.com/QuaternionNorm.html

    """
    return math.sqrt(sum([x * x for x in q]))


def quaternion_unitize(q):
    """Makes a quaternion unit-length.

    Parameters
    ----------
    q : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
        Quaternion or sequence of four floats ``[w, x, y, z]``.

    Returns
    -------
    [float, float, float, float]
        Quaternion of length 1 as a list of four real values ``[nw, nx, ny, nz]``.
    """
    n = quaternion_norm(q)
    if allclose([n], [0.0], ATOL):
        raise ValueError("The given quaternion has zero length.")
    else:
        return [x / n for x in q]


def quaternion_is_unit(q, tol=ATOL):
    """Checks if a quaternion is unit-length.

    Parameters
    ----------
    q : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
        Quaternion or sequence of four floats ``[w, x, y, z]``.
    tol : float, optional
        Requested decimal precision.

    Returns
    -------
    bool
        True if the quaternion is unit-length,
        and False if otherwise.
    """
    n = quaternion_norm(q)
    return allclose([n], [1.0], tol)


def quaternion_multiply(r, q):
    """Multiplies two quaternions.

    Parameters
    ----------
    r : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
        Quaternion or sequence of four floats ``[w, x, y, z]``.
    q : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
        Quaternion or sequence of four floats ``[w, x, y, z]``.

    Returns
    -------
    [float, float, float, float]
        Quaternion :math:`p = rq` as a list of four real values ``[pw, px, py, pz]``.

    Notes
    -----
    Multiplication of two quaternions :math:`p = rq` can be interpreted as applying rotation :math:`r` to an orientation :math:`q`,
    provided that both :math:`r` and :math:`q` are unit-length.
    The result is also unit-length.
    Multiplication of quaternions is not commutative!

    References
    ----------
    * Quaternion: http://mathworld.wolfram.com/Quaternion.html

    """
    rw, rx, ry, rz = r
    qw, qx, qy, qz = q
    pw = rw * qw - rx * qx - ry * qy - rz * qz
    px = rw * qx + rx * qw + ry * qz - rz * qy
    py = rw * qy - rx * qz + ry * qw + rz * qx
    pz = rw * qz + rx * qy - ry * qx + rz * qw
    return [pw, px, py, pz]


def quaternion_canonize(q):
    """Converts a quaternion into a canonic form if needed.

    Parameters
    ----------
    q : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
        Quaternion or sequence of four floats ``[w, x, y, z]``.

    Returns
    -------
    [float, float, float, float]
        Quaternion in a canonic form as a list of four real values ``[cw, cx, cy, cz]``.

    Notes
    -----
    Canonic form means the scalar component is a non-negative number.

    """
    if q[0] < 0.0:
        return [-x for x in q]
    return q[:]


def quaternion_conjugate(q):
    """Conjugate of a quaternion.

    Parameters
    ----------
    q : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
        Quaternion or sequence of four floats ``[w, x, y, z]``.

    Returns
    -------
    [float, float, float, float]
        Conjugate quaternion as a list of four real values ``[cw, cx, cy, cz]``.

    References
    ----------
    *  Quaternion Conjugate: http://mathworld.wolfram.com/QuaternionConjugate.html

    """
    return [q[0], -q[1], -q[2], -q[3]]


# src/compas/geometry/_core/size.py

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

from math import fabs

from compas.utilities import pairwise

from ._algebra import subtract_vectors
from ._algebra import subtract_vectors_xy
from ._algebra import length_vector
from ._algebra import cross_vectors
from ._algebra import cross_vectors_xy
from ._algebra import dot_vectors

from .centroids import centroid_points
from .centroids import centroid_points_xy

from .normals import normal_triangle
from .normals import normal_triangle_xy


def area_polygon(polygon):
    """Compute the area of a polygon.

    Parameters
    ----------
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        The XYZ coordinates of the vertices/corners of the polygon.
        The vertices are assumed to be in order.
        The polygon is assumed to be closed:
        the first and last vertex in the sequence should not be the same.

    Returns
    -------
    float
        The area of the polygon.

    Examples
    --------
    >>>
    """
    o = centroid_points(polygon)
    a = polygon[-1]
    b = polygon[0]
    oa = subtract_vectors(a, o)
    ob = subtract_vectors(b, o)
    n0 = cross_vectors(oa, ob)
    area = 0.5 * length_vector(n0)
    for i in range(0, len(polygon) - 1):
        oa = ob
        b = polygon[i + 1]
        ob = subtract_vectors(b, o)
        n = cross_vectors(oa, ob)
        if dot_vectors(n, n0) > 0:
            area += 0.5 * length_vector(n)
        else:
            area -= 0.5 * length_vector(n)
    return abs(area)


def area_polygon_xy(polygon):
    """Compute the area of a polygon lying in the XY-plane.

    Parameters
    ----------
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        A sequence of XY(Z) coordinates of 2D or 3D points
        representing the locations of the corners of a polygon.
        The vertices are assumed to be in order. The polygon is assumed to be closed:
        the first and last vertex in the sequence should not be the same.

    Returns
    -------
    float
        The area of the polygon.

    """
    o = centroid_points_xy(polygon)
    u = subtract_vectors_xy(polygon[-1], o)
    v = subtract_vectors_xy(polygon[0], o)
    a = 0.5 * cross_vectors_xy(u, v)[2]
    for i in range(0, len(polygon) - 1):
        u = v
        v = subtract_vectors_xy(polygon[i + 1], o)
        a += 0.5 * cross_vectors_xy(u, v)[2]
    return fabs(a)


def area_triangle(triangle):
    """Compute the area of a triangle defined by three points.

    Parameters
    ----------
    triangle : [point, point, point] | :class:`~compas.geometry.Polygon`
        XYZ coordinates of the corners of the triangle.

    Returns
    -------
    float
        The area of the triangle.

    """
    return 0.5 * length_vector(normal_triangle(triangle, False))


def area_triangle_xy(triangle):
    """Compute the area of a triangle defined by three points lying in the XY-plane.

    Parameters
    ----------
    triangle : [point, point, point] | :class:`~compas.geometry.Polygon`
        XY(Z) coordinates of the corners of the triangle.

    Returns
    -------
    float
        The area of the triangle.

    """
    return 0.5 * length_vector(normal_triangle_xy(triangle, False))


def volume_polyhedron(polyhedron):
    r"""Compute the volume of a polyhedron represented by a closed mesh.

    Parameters
    ----------
    polyhedron : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[sequence[int]]]
        The vertices and faces of the polyhedron.

    Returns
    -------
    float
        The volume of the polyhedron.

    Notes
    -----
    This implementation is based on the divergence theorem, the fact that the
    *area vector* is constant for each face, and the fact that the area of each
    face can be computed as half the length of the cross product of two adjacent
    edge vectors [1]_.

    .. math::
        :nowrap:

        \begin{align}
            V  = \int_{P} 1
              &= \frac{1}{3} \int_{\partial P} \mathbf{x} \cdot \mathbf{n} \\
              &= \frac{1}{3} \sum_{i=0}^{N-1} \int{A_{i}} a_{i} \cdot n_{i} \\
              &= \frac{1}{6} \sum_{i=0}^{N-1} a_{i} \cdot \hat n_{i}
        \end{align}

    Warnings
    --------
    The volume computed by this funtion is only correct if the polyhedron is convex,
    has planar faces, and is positively oriented (all face normals point outwards).

    References
    ----------
    .. [1] Nurnberg, R. *Calculating the area and centroid of a polygon in 2d*.
           Available at: http://wwwf.imperial.ac.uk/~rn/centroid.pdf

    """
    xyz, faces = polyhedron

    V = 0
    for vertices in faces:
        if len(vertices) == 3:
            triangles = [vertices]
        else:
            centroid = centroid_points([xyz[i] for i in vertices])
            i = len(xyz)
            xyz.append(centroid)
            triangles = []
            for u, v in pairwise(vertices + vertices[0:1]):
                triangles.append([i, u, v])

        for u, v, w in triangles:
            a = xyz[u]
            b = xyz[v]
            c = xyz[w]
            ab = subtract_vectors(b, a)
            ac = subtract_vectors(c, a)
            n = cross_vectors(ab, ac)
            V += dot_vectors(a, n)
    return V / 6.0


# src/compas/geometry/_core/tangent.py

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

from math import sqrt


def tangent_points_to_circle_xy(circle, point):
    """Calculates the tangent points on a circle in the XY plane.

    Parameters
    ----------
    circle : [plane, float] | :class:`~compas.geometry.Circle`
        Plane and radius of the circle.
    point : [float, float] or [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point in the xy plane.

    Returns
    -------
    tuple[[float, float, 0.0], [float, float, 0.0]]
        the tangent points on the circle.

    Examples
    --------
    >>> from compas.geometry import allclose
    >>> circle = ((0, 0, 0), (0, 0, 1)), 1.0
    >>> point = (2, 4, 0)
    >>> t1, t2 = tangent_points_to_circle_xy(circle, point)
    >>> allclose(t1, [-0.772, 0.636, 0.000], 1e-3)
    True
    >>> allclose(t2, [0.972, -0.236, 0.000], 1e-3)
    True
    """
    plane, R = circle
    center, _ = plane

    cx, cy = center[:2]
    px, py = point[:2]

    dx = px - cx
    dy = py - cy

    D = sqrt(dx**2 + dy**2)
    L2 = D**2 - R**2

    a = dx / D, dy / D
    b = -a[1], a[0]

    A = D - L2 / D
    B = sqrt(R**2 - A**2)

    t1 = cx + A * a[0] + B * b[0], cy + A * a[1] + B * b[1], 0
    t2 = cx + A * a[0] - B * b[0], cy + A * a[1] - B * b[1], 0

    return t1, t2


# src/compas/geometry/_core/transformations.py

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

import math

from ._algebra import scale_vector
from ._algebra import scale_vector_xy
from ._algebra import normalize_vector
from ._algebra import add_vectors
from ._algebra import add_vectors_xy
from ._algebra import subtract_vectors
from ._algebra import subtract_vectors_xy
from ._algebra import dot_vectors
from ._algebra import cross_vectors
from ._algebra import vector_component
from ._algebra import vector_component_xy
from ._algebra import multiply_matrix_vector
from ._algebra import multiply_matrices
from ._algebra import transpose_matrix
from ._algebra import norm_vector
from .angles import angle_vectors
from .distance import closest_point_on_plane
from .distance import closest_point_on_line
from .distance import closest_point_on_line_xy
from .matrices import matrix_from_axis_and_angle
from .matrices import matrix_from_scale_factors
from .matrices import matrix_from_change_of_basis


# this function will not always work
# it is also a duplicate of stuff found in matrices and frame
def local_axes(a, b, c):
    u = b - a
    v = c - a
    w = cross_vectors(u, v)
    v = cross_vectors(w, u)
    return normalize_vector(u), normalize_vector(v), normalize_vector(w)


def orthonormalize_axes(xaxis, yaxis):
    """Corrects xaxis and yaxis to be unit vectors and orthonormal.

    Parameters
    ----------
    xaxis: [float, float, float] | :class:`~compas.geometry.Vector`
        The first axis.
    yaxis: [float, float, float] | :class:`~compas.geometry.Vector`
        The second axis.

    Returns
    -------
    [float, float, float]
        The corrected x axis.
    [float, float, float]
        The corrected y axis.

    Raises
    ------
    ValueError
        If xaxis and yaxis cannot span a plane.

    Examples
    --------
    >>> from compas.geometry import allclose
    >>> xaxis = [1, 4, 5]
    >>> yaxis = [1, 0, -2]
    >>> xaxis, yaxis = orthonormalize_axes(xaxis, yaxis)
    >>> allclose(xaxis, [0.1543, 0.6172, 0.7715], tol=0.001)
    True
    >>> allclose(yaxis, [0.6929, 0.4891, -0.5298], tol=0.001)
    True

    """
    xaxis = normalize_vector(xaxis)
    yaxis = normalize_vector(yaxis)
    zaxis = cross_vectors(xaxis, yaxis)
    if not norm_vector(zaxis):
        raise ValueError("Xaxis and yaxis cannot span a plane.")
    yaxis = cross_vectors(normalize_vector(zaxis), xaxis)
    return xaxis, yaxis


def homogenize(xyz, w=1.0):
    """Homogenise a list of vectors.

    Parameters
    ----------
    xyz : sequence[[float, float, float] | :class:`~compas.geometry.Point`] | sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of points or vectors.
    w : float, optional
        Homogenisation parameter.
        Use ``1.0`` for points, and ``0.0`` for vectors.

    Returns
    -------
    list[[float, float, float, `w`]]
        Homogenised data.

    Notes
    -----
    Vectors described by XYZ components are homogenised by appending a homogenisation
    parameter to the components, and by dividing each component by that parameter.
    Homogenisatioon of vectors is often used in relation to transformations.

    Examples
    --------
    >>> vectors = [[1.0, 0.0, 0.0]]
    >>> homogenize(vectors)
    [[1.0, 0.0, 0.0, 1.0]]

    """
    return [[x * w, y * w, z * w, w] if w else [x, y, z, 0.0] for x, y, z in xyz]


def dehomogenize(xyzw):
    """Dehomogenise a list of vectors.

    Parameters
    ----------
    xyzw : sequence[[float, float, float, `w`]]
        A list of vectors.

    Returns
    -------
    list[[float, float, float]]
        Dehomogenised vectors.

    Examples
    --------
    >>>

    """
    return [[x / w, y / w, z / w] if w else [x, y, z] for x, y, z, w in xyzw]


def homogenize_and_flatten_frames(frames):
    """Homogenize a list of frames and flatten the 3D list into a 2D list.

    Parameters
    ----------
    frames : sequence[[point, vector, vector] | :class:`~compas.geometry.Frame`]

    Returns
    -------
    list[[float, float, float, `w`]]
        A list with 3 entries per frame: a homogenized point, and two homogenized vectors.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> frames = [Frame((1, 1, 1), (0, 1, 0), (1, 0, 0))]
    >>> homogenize_and_flatten_frames(frames)
    [[1.0, 1.0, 1.0, 1.0], [0.0, 1.0, 0.0, 0.0], [1.0, -0.0, 0.0, 0.0]]

    """

    def homogenize_frame(frame):
        return homogenize([frame[0]], w=1.0) + homogenize([frame[1], frame[2]], w=0.0)

    return [v for frame in frames for v in homogenize_frame(frame)]


def dehomogenize_and_unflatten_frames(points_and_vectors):
    """Dehomogenize a list of vectors and unflatten the 2D list into a 3D list.

    Parameters
    ----------
    points_and_vectors : sequence[[float, float, float, `w`]]
        List of homogenized frames with 3 entries per frame: a homogenized point, and two homogenized vectors.

    Returns
    -------
    list[[point, vector, vector]]
        The dehmogenized frame data.

    Examples
    --------
    >>> points_and_vectors = [(1., 1., 1., 1.), (0., 1., 0., 0.), (1., 0., 0., 0.)]
    >>> dehomogenize_and_unflatten_frames(points_and_vectors)
    [[[1.0, 1.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]]

    """
    frames = dehomogenize(points_and_vectors)
    return [frames[i : i + 3] for i in range(0, len(frames), 3)]


# ==============================================================================
# transform
# ==============================================================================


def transform_points(points, T):
    """Transform multiple points with one transformation matrix.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points to be transformed.
    T : list[list[float]] | :class:`~compas.geometry.Transformation`
        The transformation to apply.

    Returns
    -------
    list[[float, float, float]]
        Transformed points.

    Examples
    --------
    >>> points = [[1, 0, 0], [1, 2, 4], [4, 7, 1]]
    >>> T = matrix_from_axis_and_angle([0, 2, 0], math.radians(45), point=[4, 5, 6])
    >>> points_transformed = transform_points(points, T)

    """
    return dehomogenize(multiply_matrices(homogenize(points, w=1.0), transpose_matrix(T)))


def transform_vectors(vectors, T):
    """Transform multiple vectors with one transformation matrix.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors to be transformed.
    T : list[list[float]] | :class:`~compas.geometry.Transformation`
        The transformation to apply.

    Returns
    -------
    list[[float, float, float]]
        Transformed vectors.

    Examples
    --------
    >>> vectors = [[1, 0, 0], [1, 2, 4], [4, 7, 1]]
    >>> T = matrix_from_axis_and_angle([0, 2, 0], math.radians(45), point=[4, 5, 6])
    >>> vectors_transformed = transform_vectors(vectors, T)

    """
    return dehomogenize(multiply_matrices(homogenize(vectors, w=0.0), transpose_matrix(T)))


def transform_frames(frames, T):
    """Transform multiple frames with one transformation matrix.

    Parameters
    ----------
    frames : sequence[[point, vector, vector] | :class:`~compas.geometry.Frame`]
        A list of frames to be transformed.
    T : list[list[float]] | :class:`~compas.geometry.Transformation`
        The transformation to apply on the frames.

    Returns
    -------
    list[[point, vector, vector]]
        Transformed frames.

    Examples
    --------
    >>> from compas.geometry import Frame, matrix_from_axis_and_angle
    >>> frames = [Frame([1, 0, 0], [1, 2, 4], [4, 7, 1]), Frame([0, 2, 0], [5, 2, 1], [0, 2, 1])]
    >>> T = matrix_from_axis_and_angle([0, 2, 0], math.radians(45), point=[4, 5, 6])
    >>> transformed_frames = transform_frames(frames, T)

    """
    points_and_vectors = homogenize_and_flatten_frames(frames)
    return dehomogenize_and_unflatten_frames(multiply_matrices(points_and_vectors, transpose_matrix(T)))


def world_to_local_coordinates(frame, xyz):
    """Convert global coordinates to local coordinates.

    Parameters
    ----------
    frame : [point, vector, vector] | :class:`~compas.geometry.Frame`
        The local coordinate system.
    xyz : array-like[[float, float, float] | :class:`~compas.geometry.Point`]
        The global coordinates of the points to convert.

    Returns
    -------
    list[[float, float, float]]
        The coordinates of the given points in the local coordinate system.

    Examples
    --------
    >>> from compas.geometry import Point, Frame
    >>> f = Frame([0, 1, 0], [3, 4, 1], [1, 5, 9])
    >>> xyz = [Point(2, 3, 5)]
    >>> Point(*world_to_local_coordinates(f, xyz)[0])
    Point(3.726, 4.088, 1.550)

    """
    from compas.geometry import Frame  # noqa: F811

    T = matrix_from_change_of_basis(Frame.worldXY(), frame)
    return transform_points(xyz, T)


def local_to_world_coordinates(frame, xyz):
    """Convert local coordinates to global coordinates.

    Parameters
    ----------
    frame : [point, vector, vector] | :class:`~compas.geometry.Frame`
        The local coordinate system.
    xyz : array-like[[float, float, float] | :class:`~compas.geometry.Point`]
        The global coordinates of the points to convert.

    Returns
    -------
    list[[float, float, float]]
        The coordinates of the given points in the local coordinate system.

    Examples
    --------
    >>> from compas.geometry import Point, Frame
    >>> f = Frame([0, 1, 0], [3, 4, 1], [1, 5, 9])
    >>> xyz = [Point(3.726, 4.088, 1.550)]
    >>> Point(*local_to_world_coordinates(f, xyz)[0])
    Point(2.000, 3.000, 5.000)

    """
    from compas.geometry import Frame  # noqa: F811

    T = matrix_from_change_of_basis(frame, Frame.worldXY())
    return transform_points(xyz, T)


# ==============================================================================
# translate
# ==============================================================================


def translate_points(points, vector):
    """Translate points.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points.
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        A translation vector.

    Returns
    -------
    list[[float, float, float]]
        The translated points.

    Examples
    --------
    >>>

    """
    return [add_vectors(point, vector) for point in points]


def translate_points_xy(points, vector):
    """Translate points and in the XY plane.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points.
    vector : [float, float, float] | :class:`~compas.geometry.Vector`
        A translation vector.

    Returns
    -------
    list[[float, float, float]]
        The translated points in the XY plane (Z=0).

    Examples
    --------
    >>>

    """
    return [add_vectors_xy(point, vector) for point in points]


# ==============================================================================
# scale
# ==============================================================================


def scale_points(points, scale):
    """Scale points.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points.
    scale : float
        A scaling factor.

    Returns
    -------
    list[[float, float, float]]
        The scaled points.

    Examples
    --------
    >>>

    """
    T = matrix_from_scale_factors([scale, scale, scale])
    return transform_points(points, T)


def scale_points_xy(points, scale):
    """Scale points in the XY plane.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points.
    scale : float
        A scaling factor.

    Returns
    -------
    list[[float, float, float]]
        The scaled points in the XY plane (Z=0).

    Examples
    --------
    >>>

    """
    T = matrix_from_scale_factors([scale, scale, 0])
    return transform_points(points, T)


# ==============================================================================
# rotate
# ==============================================================================


def rotate_points(points, angle, axis=None, origin=None):
    """Rotates points around an arbitrary axis in 3D.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points.
    angle : float
        The angle of rotation in radians.
    axis : [float, float, float] | :class:`~compas.geometry.Vector`, optional
        The rotation axis.
        Default is ``[0.0, 0.0, 1.0]``
    origin : [float, float, float] | :class:`~compas.geometry.Point`, optional
        The origin of the rotation axis.
        Default is ``[0.0, 0.0, 0.0]``.

    Returns
    -------
    list[[float, float, float]]
        The rotated points

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wikipedia. *Rotation matrix*.
           Available at: https://en.wikipedia.org/wiki/Rotation_matrix.

    Examples
    --------
    >>>

    """
    if axis is None:
        axis = [0.0, 0.0, 1.0]
    if origin is None:
        origin = [0.0, 0.0, 0.0]

    R = matrix_from_axis_and_angle(axis, angle, origin)
    points = transform_points(points, R)
    return points


def rotate_points_xy(points, angle, origin=None):
    """Rotates points in the XY plane around the Z axis at a specific origin.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points.
    angle : float
        The angle of rotation in radians.
    origin : [float, float, float] | :class:`~compas.geometry.Point`, optional
        The origin of the rotation axis.
        Default is ``[0.0, 0.0, 0.0]``.

    Returns
    -------
    list[[float, float, 0.0]]
        The rotated points in the XY plane (Z=0).

    Examples
    --------
    >>>

    """
    if not origin:
        origin = [0.0, 0.0, 0.0]

    cosa = math.cos(angle)
    sina = math.sin(angle)
    R = [[cosa, -sina, 0.0], [sina, cosa, 0.0], [0.0, 0.0, 1.0]]
    # translate points
    points = translate_points_xy(points, scale_vector_xy(origin, -1.0))
    # rotate points
    points = [multiply_matrix_vector(R, point) for point in points]
    # translate points back
    points = translate_points_xy(points, origin)
    return points


# ==============================================================================
# mirror
# ==============================================================================


def mirror_vector_vector(v1, v2):
    """Mirrors vector about vector.

    Parameters
    ----------
    v1 : [float, float, float] | :class:`~compas.geometry.Vector`
        The vector.
    v2 : [float, float, float] | :class:`~compas.geometry.Vector`
        The normalized vector as mirror axis

    Returns
    -------
    [float, float, float]
        The mirrored vector.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Math Stack Exchange. *How to get a reflection vector?*
           Available at: https://math.stackexchange.com/questions/13261/how-to-get-a-reflection-vector.

    """
    return subtract_vectors(v1, scale_vector(v2, 2 * dot_vectors(v1, v2)))


def mirror_point_point(point, mirror):
    """Mirror a point about a point.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the point to mirror.
    mirror : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the mirror point.

    Returns
    -------
    [float, float, float]
        The mirrored point.

    """
    return add_vectors(mirror, subtract_vectors(mirror, point))


def mirror_point_point_xy(point, mirror):
    """Mirror a point about a point.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the point to mirror.
    mirror : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the mirror point.

    Returns
    -------
    [float, float, float]
        The mirrored point, with Z=0.

    """
    return add_vectors_xy(mirror, subtract_vectors_xy(mirror, point))


def mirror_points_point(points, mirror):
    """Mirror multiple points about a point.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        List of points.
    mirror : [float, float, float] | :class:`~compas.geometry.Point`
       The mirror point.

    Returns
    -------
    list[[float, float, float]]
        The mirrored points, with Z=0.

    """
    return [mirror_point_point(point, mirror) for point in points]


def mirror_points_point_xy(points, mirror):
    """Mirror multiple points about a point.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        List of points with XY(Z) coordinates.
    mirror : [float, float, float] | :class:`~compas.geometry.Point`
       The XY(Z) coordinates of the mirror point.

    Returns
    -------
    list[[float, float, float]]
        The mirrored points, with Z=0.

    """
    return [mirror_point_point_xy(point, mirror) for point in points]


def mirror_point_line(point, line):
    """Mirror a point about a line.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the point to mirror.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the mirror line.

    Returns
    -------
    [float, float, float]
        The mirrored point.

    """
    closest = closest_point_on_line(point, line)
    return add_vectors(closest, subtract_vectors(closest, point))


def mirror_point_line_xy(point, line):
    """Mirror a point about a line.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the point to mirror.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the line.
        XY(Z) coordinates of the two points defining the mirror line.

    Returns
    -------
    [float, float, float]
        The mirrored point, with Z=0.

    """
    closest = closest_point_on_line_xy(point, line)
    return add_vectors_xy(closest, subtract_vectors_xy(closest, point))


def mirror_points_line(points, line):
    """Mirror a point about a line.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        List of points to mirror.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the mirror line.

    Returns
    -------
    list[[float, float, float]]
        The mirrored points.

    """
    return [mirror_point_line(point, line) for point in points]


def mirror_points_line_xy(points, line):
    """Mirror a point about a line.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        List of points to mirror.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the mirror line.

    Returns
    -------
    list[[float, float, float]]
        The mirrored points.

    """
    return [mirror_point_line_xy(point, line) for point in points]


def mirror_point_plane(point, plane):
    """Mirror a point about a plane.

    Parameters
    ----------
    point : list[float]
        XYZ coordinates of mirror point.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        Base point and normal defining the mirror plane.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the mirrored point.

    """
    closest = closest_point_on_plane(point, plane)
    return add_vectors(closest, subtract_vectors(closest, point))


def mirror_points_plane(points, plane):
    """Mirror a point about a plane.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        List of points to mirror.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        Base point and normal defining the mirror plane.

    Returns
    -------
    list[[float, float, float]]
        The mirrored points.

    """
    return [mirror_point_plane(point, plane) for point in points]


# ==============================================================================
# project
# specify orhtogonal
# add perspective
# ==============================================================================


def project_point_plane(point, plane):
    """Project a point onto a plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the point.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        Base point and normal vector defining the projection plane.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the projected point.

    Notes
    -----
    The projection is in the direction perpendicular to the plane.
    The projected point is thus the closest point on the plane to the original
    point [1]_.

    References
    ----------
    .. [1] Math Stack Exchange. *Project a point in 3D on a given plane*.
           Available at: https://math.stackexchange.com/questions/444968/project-a-point-in-3d-on-a-given-plane.

    Examples
    --------
    >>> from compas.geometry import project_point_plane
    >>> point = [3.0, 3.0, 3.0]
    >>> plane = ([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])  # the XY plane
    >>> project_point_plane(point, plane)
    [3.0, 3.0, 0.0]

    """
    base, normal = plane
    normal = normalize_vector(normal)
    vector = subtract_vectors(point, base)
    snormal = scale_vector(normal, dot_vectors(vector, normal))
    return subtract_vectors(point, snormal)


def project_points_plane(points, plane):
    """Project multiple points onto a plane.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        List of points.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        Base point and normal vector defining the projection plane.

    Returns
    -------
    list[[float, float, float]]
        The projected points.

    See Also
    --------
    project_point_plane

    """
    return [project_point_plane(point, plane) for point in points]


def project_point_line(point, line):
    """Project a point onto a line.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XYZ coordinates of the point.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the projection line.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the projected point.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wiki Books. *Linear Algebra/Orthogonal Projection Onto a Line*.
           Available at: https://en.wikibooks.org/wiki/Linear_Algebra/Orthogonal_Projection_Onto_a_Line.

    """
    a, b = line
    ab = subtract_vectors(b, a)
    ap = subtract_vectors(point, a)
    c = vector_component(ap, ab)

    return add_vectors(a, c)


def project_point_line_xy(point, line):
    """Project a point onto a line in the XY plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the point.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the projection line.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the projected point, with Z=0.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wiki Books. *Linear Algebra/Orthogonal Projection Onto a Line*.
           Available at: https://en.wikibooks.org/wiki/Linear_Algebra/Orthogonal_Projection_Onto_a_Line.

    """
    a, b = line
    ab = subtract_vectors_xy(b, a)
    ap = subtract_vectors_xy(point, a)
    c = vector_component_xy(ap, ab)
    return add_vectors_xy(a, c)


def project_points_line(points, line):
    """Project points onto a line.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        XYZ coordinates of the points.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the projection line.

    Returns
    -------
    list[[float, float, float]]
        XYZ coordinates of the projected points.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wiki Books. *Linear Algebra/Orthogonal Projection Onto a Line*.
           Available at: https://en.wikibooks.org/wiki/Linear_Algebra/Orthogonal_Projection_Onto_a_Line.

    """
    return [project_point_line(point, line) for point in points]


def project_points_line_xy(points, line):
    """Project points onto a line in the XY plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of the point.
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the projection line.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the projected point, with Z=0.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Wiki Books. *Linear Algebra/Orthogonal Projection Onto a Line*.
           Available at: https://en.wikibooks.org/wiki/Linear_Algebra/Orthogonal_Projection_Onto_a_Line.

    """
    return [project_point_line_xy(point, line) for point in points]


# ==============================================================================
# reflection
# ==============================================================================


def reflect_line_plane(line, plane, tol=1e-6):
    """Bounce a line of a reflection plane.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the line.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        Base point and normal vector of the plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]]
        The reflected line defined by the intersection point of the line and plane
        and the mirrored start point of the line with respect to a line perpendicular
        to the plane through the intersection.

    Notes
    -----
    The direction of the line and plane are important.
    The line is only reflected if it points towards the front of the plane.
    This is true if the dot product of the direction vector of the line and the
    normal vector of the plane is smaller than zero.

    Examples
    --------
    >>> plane = [0, 0, 0], [0, 1, 0]
    >>> line = [-1, 1, 0], [-0.5, 0.5, 0]
    >>> reflect_line_plane(line, plane)
    ([0.0, 0.0, 0.0], [1.0, 1.0, 0.0])

    """
    from compas.geometry import intersection_line_plane

    x = intersection_line_plane(line, plane, tol=tol)
    if not x:
        return

    a, b = line
    o, n = plane
    ab = subtract_vectors(b, a)

    if dot_vectors(ab, n) > 0:
        # the line does not point towards the front of the plane
        return

    mirror = x, add_vectors(x, n)
    return x, mirror_point_line(a, mirror)


def reflect_line_triangle(line, triangle, tol=1e-6):
    """Bounce a line of a reflection triangle.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the line.
    triangle : [point, point, point]
        The triangle vertices.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]]
        The reflected line defined by the intersection point of the line and triangle
        and the mirrored start point of the line with respect to a line perpendicular
        to the triangle through the intersection.

    Notes
    -----
    The direction of the line and triangle are important.
    The line is only reflected if it points towards the front of the triangle.
    This is true if the dot product of the direction vector of the line and the
    normal vector of the triangle is smaller than zero.

    Examples
    --------
    >>> triangle = [1.0, 0, 0], [-1.0, 0, 0], [0, 0, 1.0]
    >>> line = [-1, 1, 0], [-0.5, 0.5, 0]
    >>> reflect_line_triangle(line, triangle)
    ([0.0, 0.0, 0.0], [1.0, 1.0, 0.0])

    """
    from compas.geometry import intersection_line_triangle

    x = intersection_line_triangle(line, triangle, tol=tol)
    if not x:
        return

    a, b = line
    t1, t2, t3 = triangle
    ab = subtract_vectors(b, a)
    n = cross_vectors(subtract_vectors(t2, t1), subtract_vectors(t3, t1))

    if dot_vectors(ab, n) > 0:
        # the line does not point towards the front of the triangle
        return

    mirror = x, add_vectors(x, n)
    return x, mirror_point_line(a, mirror)


# ==============================================================================
# shear
# ==============================================================================


# ==============================================================================
# orientation
# ==============================================================================


def orient_points(points, reference_plane, target_plane):
    """Orient points from one plane to another.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]s
        XYZ coordinates of the points.
    reference_plane : [point, vector] | :class:`~compas.geometry.Plane`
        Base point and normal defining a reference plane.
    target_plane : [point, vector] | :class:`~compas.geometry.Plane`
        Base point and normal defining a target plane.

    Returns
    -------
    list[[float, float, float]]
        XYZ coordinates of the oriented points.

    Notes
    -----
    This function is useful to orient a planar problem in the xy-plane to simplify
    the calculation (see example).

    Examples
    --------
    >>> from compas.geometry import Point
    >>> from compas.geometry import orient_points
    >>> from compas.geometry import intersection_segment_segment_xy

    >>> refplane = ([0.57735, 0.57735, 0.57735], [1.0, 1.0, 1.0])
    >>> tarplane = ([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])
    >>> points = [\
            [0.288675, 0.288675, 1.1547],\
            [0.866025, 0.866025, 0.0],\
            [1.077350, 0.077350, 0.57735],\
            [0.077350, 1.077350, 0.57735]\
        ]

    >>> points = orient_points(points, refplane, tarplane)
    >>> ab = points[0], points[1]
    >>> cd = points[2], points[3]
    >>> point = intersection_segment_segment_xy(ab, cd)
    >>> points = orient_points([point], tarplane, refplane)

    >>> Point(*points[0])
    Point(0.577, 0.577, 0.577)

    """
    axis = cross_vectors(reference_plane[1], target_plane[1])
    angle = angle_vectors(reference_plane[1], target_plane[1])
    origin = reference_plane[0]

    if angle:
        points = rotate_points(points, angle, axis, origin)

    vector = subtract_vectors(target_plane[0], reference_plane[0])
    points = translate_points(points, vector)

    return points


# src/compas/geometry/_core/transformations_numpy.py

from numpy import asarray
from numpy import hstack
from numpy import ones
from numpy import vectorize
from numpy import tile

from scipy.linalg import solve  # type: ignore

from ._algebra import cross_vectors


def transform_points_numpy(points, T):
    """Transform multiple points with one Transformation using numpy.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        A list of points to be transformed.
    T : :class:`~compas.geometry.Transformation` | list[list[float]]
        The transformation to apply.

    Returns
    -------
    (N, 3) ndarray
        The transformed points.

    Examples
    --------
    >>> from compas.geometry import matrix_from_axis_and_angle
    >>> points = [[1, 0, 0], [1, 2, 4], [4, 7, 1]]
    >>> T = matrix_from_axis_and_angle([0, 2, 0], math.radians(45), point=[4, 5, 6])
    >>> points_transformed = transform_points_numpy(points, T)

    """
    T = asarray(T)
    points = homogenize_numpy(points, w=1.0)
    return dehomogenize_numpy(points.dot(T.T))


def transform_vectors_numpy(vectors, T):
    """Transform multiple vectors with one Transformation using numpy.

    Parameters
    ----------
    vectors : sequence[[float, float, float] | :class:`~compas.geometry.Vector`]
        A list of vectors to be transformed.
    T : :class:`~compas.geometry.Transformation`
        The transformation to apply.

    Returns
    -------
    (N, 3) ndarray
        The transformed vectors.

    Examples
    --------
    >>> from compas.geometry import matrix_from_axis_and_angle
    >>> vectors = [[1, 0, 0], [1, 2, 4], [4, 7, 1]]
    >>> T = matrix_from_axis_and_angle([0, 2, 0], math.radians(45), point=[4, 5, 6])
    >>> vectors_transformed = transform_vectors_numpy(vectors, T)

    """
    T = asarray(T)
    vectors = homogenize_numpy(vectors, w=0.0)
    return dehomogenize_numpy(vectors.dot(T.T))


def transform_frames_numpy(frames, T):
    """Transform multiple frames with one Transformation usig numpy.

    Parameters
    ----------
    frames : sequence[[point, vector, vector] | :class:`~compas.geometry.Frame`]
        A list of frames to be transformed.
    T : :class:`~compas.geometry.Transformation`
        The transformation to apply on the frames.

    Returns
    -------
    (N, 3, 3) ndarray
        The transformed frames.

    Examples
    --------
    >>> from compas.geometry import Frame, matrix_from_axis_and_angle
    >>> frames = [Frame([1, 0, 0], [1, 2, 4], [4, 7, 1]), Frame([0, 2, 0], [5, 2, 1], [0, 2, 1])]
    >>> T =  matrix_from_axis_and_angle([0, 2, 0], math.radians(45), point=[4, 5, 6])
    >>> transformed_frames = transform_frames_numpy(frames, T)

    """
    T = asarray(T)
    points_and_vectors = homogenize_and_flatten_frames_numpy(frames)
    return dehomogenize_and_unflatten_frames_numpy(points_and_vectors.dot(T.T))


def world_to_local_coordinates_numpy(frame, xyz):
    """Convert global coordinates to local coordinates.

    Parameters
    ----------
    frame : [point, vector, vector] | :class:`~compas.geometry.Frame`
        The local coordinate system.
    xyz : array-like[[float, float, float] | :class:`~compas.geometry.Point`]
        The global coordinates of the points to convert.

    Returns
    -------
    (N, 3) ndarray
        The coordinates of the given points in the local coordinate system.

    Examples
    --------
    >>> from compas.geometry import Point, Frame
    >>> frame = Frame([0, 1, 0], [3, 4, 1], [1, 5, 9])
    >>> xyz = [Point(2, 3, 5)]
    >>> rst = world_to_local_coordinates_numpy(frame, xyz)
    >>> np.allclose(rst, [[3.726, 4.088, 1.550]], rtol=1e-3)
    True

    """
    origin = frame[0]
    uvw = [frame[1], frame[2], cross_vectors(frame[1], frame[2])]
    uvw = asarray(uvw).T
    xyz = asarray(xyz).T - asarray(origin).reshape((-1, 1))
    rst = solve(uvw, xyz)
    return rst.T


def local_to_world_coordinates_numpy(frame, rst):
    """Convert local coordinates to global (world) coordinates.

    Parameters
    ----------
    frame : [point, vector, vector] | :class:`~compas.geometry.Frame`
        The local coordinate system.
    rst : array-like[[float, float, float] | :class:`~compas.geometry.Point`]
        The coordinates of the points wrt the local coordinate system.

    Returns
    -------
    (N, 3) ndarray
        The world coordinates of the given points.

    Notes
    -----
    `origin` and `uvw` together form the frame of local coordinates.

    Examples
    --------
    >>> from compas.geometry import Point, Frame
    >>> frame = Frame([0, 1, 0], [3, 4, 1], [1, 5, 9])
    >>> rst = [Point(3.726, 4.088, 1.550)]
    >>> xyz = local_to_world_coordinates_numpy(frame, rst)
    >>> np.allclose(xyz, [[2.000, 3.000, 5.000]], rtol=1e-3)
    True

    """
    origin = frame[0]
    uvw = [frame[1], frame[2], cross_vectors(frame[1], frame[2])]

    uvw = asarray(uvw).T
    rst = asarray(rst).T
    xyz = uvw.dot(rst) + asarray(origin).reshape((-1, 1))
    return xyz.T


# ==============================================================================
# helping helpers
# ==============================================================================


def homogenize_numpy(data, w=1.0):
    """Dehomogenizes points or vectors.

    Parameters
    ----------
    data : array_like[[float, float, float] | :class:`~compas.geometry.Point`] | array_like[[float, float, float] | :class:`~compas.geometry.Vector`]
        The input data.
    w : float, optional
        The homogenization factor.
        Use ``1.0`` for points, and ``0.0`` for vectors.

    Returns
    -------
    (N, 4) ndarray

    Examples
    --------
    >>> points = [[1, 1, 1], [0, 1, 0], [1, 0, 0]]
    >>> res = homogenize_numpy(points, w=1.0)
    >>> np.allclose(res, [[1.0, 1.0, 1.0, 1.0], [0.0, 1.0, 0.0, 1.0], [1.0, -0.0, 0.0, 1.0]])
    True

    """
    data = asarray(data)
    data = hstack((data, w * ones((data.shape[0], 1))))
    return data


def dehomogenize_numpy(data):
    """Dehomogenizes points or vectors.

    Parameters
    ----------
    data : array_like[[float, float, float, float]]
        The data to dehomogenize.

    Returns
    -------
    (N, 3) ndarray

    Examples
    --------
    >>> points = [[1, 1, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]]
    >>> res = dehomogenize_numpy(points)
    >>> np.allclose(res, [[1.0, 1.0, 1.0], [0.0, 1.0, 0.0], [1.0, -0.0, 0.0]])
    True

    """

    def func(a):
        return a if a else 1.0

    func = vectorize(func)

    data = asarray(data)
    return data[:, :-1] / func(data[:, -1]).reshape((-1, 1))


def homogenize_and_flatten_frames_numpy(frames):
    """Homogenize a list of frames and flatten the 3D list into a 2D list using numpy.

    Parameters
    ----------
    frames : array_like[[point, vector, vector] | :class:`~compas.geometry.Frame`]
        The input frames.

    Returns
    -------
    (N x 3, 4) ndarray
        An array of points and vectors.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> frames = [Frame((1, 1, 1), (0, 1, 0), (1, 0, 0))]
    >>> res = homogenize_and_flatten_frames_numpy(frames)
    >>> np.allclose(res, [[1.0, 1.0, 1.0, 1.0], [0.0, 1.0, 0.0, 0.0], [1.0, -0.0, 0.0, 0.0]])
    True

    """
    n = len(frames)
    frames = asarray(frames).reshape(n * 3, 3)
    extend = tile(asarray([1, 0, 0]).reshape(3, 1), (n, 1))
    return hstack((frames, extend))


def dehomogenize_and_unflatten_frames_numpy(points_and_vectors):
    """Dehomogenize a list of vectors and unflatten the 2D list into a 3D list.

    Parameters
    ----------
    points_and_vectors : array_like[[float, float, float, float]]
        Homogenized points and vectors.

    Returns
    -------
    (N / 3, 3, 3) ndarray
        The frames.

    Examples
    --------
    >>> points_and_vectors = [(1., 1., 1., 1.), (0., 1., 0., 0.), (1., 0., 0., 0.)]
    >>> res = dehomogenize_and_unflatten_frames_numpy(points_and_vectors)
    >>> np.allclose(res, [[1.0, 1.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]])
    True

    """
    frames = dehomogenize_numpy(points_and_vectors)
    return frames.reshape((int(frames.shape[0] / 3.0), 3, 3))


# src/compas/geometry/bbox/bbox.py

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

from itertools import islice


def bounding_box(points):
    """Computes the axis-aligned minimum bounding box of a list of points.

    Parameters
    ----------
    points : sequence[point]
        XYZ coordinates of the points.

    Returns
    -------
    list[[float, float, float]]
        XYZ coordinates of 8 points defining a box.

    Examples
    --------
    >>>

    """
    x, y, z = zip(*points)
    min_x = min(x)
    max_x = max(x)
    min_y = min(y)
    max_y = max(y)
    min_z = min(z)
    max_z = max(z)
    return [
        [min_x, min_y, min_z],
        [max_x, min_y, min_z],
        [max_x, max_y, min_z],
        [min_x, max_y, min_z],
        [min_x, min_y, max_z],
        [max_x, min_y, max_z],
        [max_x, max_y, max_z],
        [min_x, max_y, max_z],
    ]


def bounding_box_xy(points):
    """Compute the axis-aligned minimum bounding box of a list of points in the XY-plane.

    Parameters
    ----------
    points : sequence[point]
        XY(Z) coordinates of the points.

    Returns
    -------
    list[[float, float, 0.0]]
        XYZ coordinates of four points defining a rectangle in the XY plane.

    Notes
    -----
    This function simply ignores the Z components of the points, if it is provided.

    Examples
    --------
    >>>

    """
    x, y = islice(zip(*points), 2)
    min_x = min(x)
    max_x = max(x)
    min_y = min(y)
    max_y = max(y)
    return [
        [min_x, min_y, 0.0],
        [max_x, min_y, 0.0],
        [max_x, max_y, 0.0],
        [min_x, max_y, 0.0],
    ]


# src/compas/geometry/bbox/bbox_numpy.py

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

from numpy import array
from numpy import asarray
from numpy import argmax
from numpy import argmin
from numpy import amax
from numpy import amin
from numpy import dot
from numpy import sum

from scipy.spatial import ConvexHull

from compas.numerical import pca_numpy
from compas.geometry import local_axes
from compas.geometry import world_to_local_coordinates_numpy
from compas.geometry import local_to_world_coordinates_numpy
from compas.geometry import transform_points_numpy

from .bbox import bounding_box


# make alternative implementation using PCA
# compare results
def oriented_bounding_box_numpy(points):
    r"""Compute the oriented minimum bounding box of a set of points in 3D space.

    Parameters
    ----------
    points : array_like[point]
        XYZ coordinates of the points.

    Returns
    -------
    list[[float, float, float]]
        XYZ coordinates of 8 points defining a box.

    Raises
    ------
    AssertionError
        If the input data is 2D.
    QhullError
        If the data is essentially 2D.

    Notes
    -----
    The *oriented (minimum) bounding box* (OBB) of a given set of points
    is computed using the following procedure:

    1. Compute the convex hull of the points.
    2. For each of the faces on the hull:

       1. Compute face frame.
       2. Compute coordinates of other points in face frame.
       3. Find "peak-to-peak" (PTP) values of point coordinates along local axes.
       4. Compute volume of box formed with PTP values.

    3. Select the box with the smallest volume.

    Examples
    --------
    Generate a random set of points with
    :math:`x \in [0, 10]`, :math:`y \in [0, 1]` and :math:`z \in [0, 3]`.
    Add the corners of the box such that we now the volume is supposed to be :math:`30.0`.

    >>> points = np.random.rand(10000, 3)
    >>> bottom = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0]])
    >>> top = np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [1.0, 1.0, 1.0]])
    >>> points = np.concatenate((points, bottom, top))
    >>> points[:, 0] *= 10
    >>> points[:, 2] *= 3

    Rotate the points around an arbitrary axis by an arbitrary angle.

    >>> from compas.geometry import Rotation
    >>> from compas.geometry import transform_points_numpy
    >>> R = Rotation.from_axis_and_angle([1.0, 1.0, 0.0], 0.3 * 3.14159)
    >>> points = transform_points_numpy(points, R)

    Compute the volume of the oriented bounding box.

    >>> from compas.geometry import length_vector, subtract_vectors, close
    >>> bbox = oriented_bounding_box_numpy(points)
    >>> a = length_vector(subtract_vectors(bbox[1], bbox[0]))
    >>> b = length_vector(subtract_vectors(bbox[3], bbox[0]))
    >>> c = length_vector(subtract_vectors(bbox[4], bbox[0]))
    >>> close(a * b * c, 30.)
    True

    """
    points = asarray(points)
    n, dim = points.shape

    assert 2 < dim, "The point coordinates should be at least 3D: %i" % dim

    points = points[:, :3]

    try:
        hull = ConvexHull(points)
    except Exception:
        return oabb_numpy(points)
        # if 'QH6154' in str(e):
        #     hull = ConvexHull(points, qhull_options='Qb2:0B2:0')
        # else:
        #     raise e

    hull = ConvexHull(points)

    volume = None
    bbox = []

    # this can be vectorised!
    for simplex in hull.simplices:
        a, b, c = points[simplex]
        uvw = local_axes(a, b, c)
        xyz = points[hull.vertices]
        frame = [a, uvw[0], uvw[1]]
        rst = world_to_local_coordinates_numpy(frame, xyz)
        rmin, smin, tmin = amin(rst, axis=0)
        rmax, smax, tmax = amax(rst, axis=0)
        dr = rmax - rmin
        ds = smax - smin
        dt = tmax - tmin
        v = dr * ds * dt

        if volume is None or v < volume:
            bbox = [
                [rmin, smin, tmin],
                [rmax, smin, tmin],
                [rmax, smax, tmin],
                [rmin, smax, tmin],
                [rmin, smin, tmax],
                [rmax, smin, tmax],
                [rmax, smax, tmax],
                [rmin, smax, tmax],
            ]
            bbox = local_to_world_coordinates_numpy(frame, bbox)
            volume = v

    return bbox


def oriented_bounding_box_xy_numpy(points):
    """Compute the oriented minimum bounding box of set of points in the XY plane.

    Parameters
    ----------
    points : array_like[point]
        XY(Z) coordinates of the points.

    Returns
    -------
    list[[float, float, float]]
        XYZ coordinates of 8 points defining a box.

    Notes
    -----
    The *oriented (minimum) bounding box* (OBB) is computed using the following
    procedure:

    1. Compute the convex hull of the points.
    2. For each of the edges on the hull:

       1. Compute the s-axis as the unit vector in the direction of the edge
       2. Compute the othorgonal t-axis.
       3. Use the start point of the edge as origin.
       4. Compute the spread of the points along the s-axis. (dot product of the point vecor in local coordinates and the s-axis)
       5. Compute the spread along the t-axis.
       6. Determine the side of s on which the points are.
       7. Compute and store the corners of the bbox and its area.

    3. Select the box with the smallest area.

    Examples
    --------
    >>>

    """
    points = asarray(points)
    n, dim = points.shape

    assert 1 < dim, "The point coordinates should be at least 2D: %i" % dim

    points = points[:, :2]

    hull = ConvexHull(points)
    xy_hull = points[hull.vertices].reshape((-1, 2))

    boxes = []
    m = sum(xy_hull, axis=0) / n

    for simplex in hull.simplices:
        p0 = points[simplex[0]]
        p1 = points[simplex[1]]

        # s direction
        s = p1 - p0
        sl = sum(s**2) ** 0.5
        su = s / sl
        vn = xy_hull - p0
        sc = (sum(vn * s, axis=1) / sl).reshape((-1, 1))
        scmax = argmax(sc)
        scmin = argmin(sc)

        # box corners
        b0 = p0 + sc[scmin] * su
        b1 = p0 + sc[scmax] * su

        # t direction
        t = array([-s[1], s[0]])
        tl = sum(t**2) ** 0.5
        tu = t / tl
        vn = xy_hull - p0
        tc = (sum(vn * t, axis=1) / tl).reshape((-1, 1))
        tcmax = argmax(tc)
        tcmin = argmin(tc)

        # area
        w = sc[scmax] - sc[scmin]
        h = tc[tcmax] - tc[tcmin]
        a = w * h

        # box corners
        if dot(t, m - p0) < 0:
            b3 = b0 - h * tu
            b2 = b1 - h * tu
        else:
            b3 = b0 + h * tu
            b2 = b1 + h * tu

        # box
        boxes.append([[b0, b1, b2, b3], a[0]])

    # return the box with the smallest area
    return [point.tolist() for point in min(boxes, key=lambda b: b[1])[0]]


def oabb_numpy(points):
    """Oriented bounding box of a set of points.

    Parameters
    ----------
    points : array_like[point]
        XYZ coordinates of the points.

    Returns
    -------
    list[[float, float, float]]
        XYZ coordinates of 8 points defining a box.

    """
    from compas.geometry import Frame
    from compas.geometry import Transformation

    origin, (xaxis, yaxis, zaxis), values = pca_numpy(points)
    frame = Frame(origin, xaxis, yaxis)
    world = Frame.worldXY()
    X = Transformation.from_frame_to_frame(frame, world)
    points = transform_points_numpy(points, X)
    bbox = bounding_box(points)
    bbox = transform_points_numpy(bbox, X.inverse())
    return bbox


# src/compas/geometry/bestfit/bestfit.py

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

from compas.geometry import subtract_vectors
from compas.geometry import normalize_vector
from compas.geometry import centroid_points


def bestfit_plane(points):
    """Fit a plane to a list of (more than three) points.

    Parameters
    ----------
    points : sequence[point]
        A list of points represented by their XYZ coordinates.

    Returns
    -------
    [float, float, float]
        Base point.
    [float, float, float]
        Normal vector (normalized).

    Notes
    -----
    This method will minimize the squares of the residuals as perpendicular
    to the main axis, not the residuals perpendicular to the plane. If the
    residuals are small (i.e. your points all lie close to the resulting plane),
    then this method will probably suffice. However, if your points are more
    spread then this method may not be the best fit. For more information see
    [ernerfeldt2015]_

    References
    ----------
    .. [ernerfeldt2015] Ernerfeldt, E. *Fitting a plane to many points in 3D*.
                        Available at: http://www.ilikebigbits.com/blog/2015/3/2/plane-from-points

    Examples
    --------
    >>>

    """
    centroid = centroid_points(points)

    xx, xy, xz = 0.0, 0.0, 0.0
    yy, yz, zz = 0.0, 0.0, 0.0

    for point in points:
        rx, ry, rz = subtract_vectors(point, centroid)
        xx += rx * rx
        xy += rx * ry
        xz += rx * rz
        yy += ry * ry
        yz += ry * rz
        zz += rz * rz

    det_x = yy * zz - yz * yz
    det_y = xx * zz - xz * xz
    det_z = xx * yy - xy * xy

    det_max = max(det_x, det_y, det_z)

    if det_max == det_x:
        a = (xz * yz - xy * zz) / det_x
        b = (xy * yz - xz * yy) / det_x
        normal = (1.0, a, b)
    elif det_max == det_y:
        a = (yz * xz - xy * zz) / det_y
        b = (xy * xz - yz * xx) / det_y
        normal = (a, 1.0, b)
    else:
        a = (yz * xy - xz * yy) / det_z
        b = (xz * xy - yz * xx) / det_z
        normal = (a, b, 1.0)

    return centroid, normalize_vector(normal)


# src/compas/geometry/bestfit/bestfit_numpy.py

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

from numpy import asarray
from numpy import sqrt
from numpy import zeros
from numpy.linalg import lstsq
from scipy.optimize import leastsq

from compas.numerical import pca_numpy
from compas.geometry import world_to_local_coordinates_numpy
from compas.geometry import local_to_world_coordinates_numpy


def bestfit_plane_numpy(points):
    """Fit a plane through more than three (non-coplanar) points.

    Parameters
    ----------
    points : array_like[point]
        XYZ coordinates of the points.

    Returns
    -------
    [float, float, float]
        A point on the plane.
    [float, float, float]
        The normal vector.

    Examples
    --------
    >>>

    """
    o, uvw, _ = pca_numpy(points)
    return o, uvw[2]


def bestfit_frame_numpy(points):
    """Fit a frame to a set of points.

    Parameters
    ----------
    points : array_like[point]
        XYZ coordinates of the points.

    Returns
    -------
    [float, float, float]
        The frame origin.
    [float, float, float]
        The local X axis.
    [float, float, float]
        The local Y axis.

    Examples
    --------
    >>>

    """
    o, uvw, _ = pca_numpy(points)
    return o, uvw[0], uvw[1]


def bestfit_circle_numpy(points):
    """Fit a circle through a set of points.

    Parameters
    ----------
    points : array_like[point]
        XYZ coordinates of the points.

    Returns
    -------
    [float, float, float]
        XYZ coordinates of the center of the circle.
    [float, float, float]
        The normal vector of the local frame.
    float
        The radius of the circle.

    Notes
    -----
    The point of this function is to find the bestfit frame through the given points
    and transform the points to make the problem 2D.

    Once in 2D, the problem simplifies to finding the center point that minimises
    the difference between the resulting circles for all given points, i.e.
    minimise in the least squares sense the deviation between the individual
    radii and the average radius.

    For more information see [1]_.

    References
    ----------
    .. [1] Scipy. *Least squares circle*.
           Available at: http://scipy-cookbook.readthedocs.io/items/Least_Squares_Circle.html.

    Examples
    --------
    >>>

    """
    o, uvw, _ = pca_numpy(points)
    frame = [o, uvw[0], uvw[1]]

    rst = world_to_local_coordinates_numpy(frame, points)

    x = rst[:, 0]
    y = rst[:, 1]

    def dist(xc, yc):
        # compute the radius of the circle through each of the given points
        # for the current centre point
        return sqrt((x - xc) ** 2 + (y - yc) ** 2)

    def f(c):
        # compute the deviation of the radius of each sample point
        # from the average radius
        # => minimize this deviation
        Ri = dist(*c)
        return Ri - Ri.mean()

    # The mean of x and y are very nearly 0 (1.0e-15), which reveals a numerical
    # instability of the problem.  So, we choose our initial guess
    # to be an epsilon bigger than that.
    xm = ym = 0.00001
    c0 = xm, ym
    c, error = leastsq(f, c0)

    # compute the radius of the circle through each sample point for the
    # computed center point.
    Ri = dist(*c)

    # compute the radius of the bestfit circle as the average of the individual
    # radii.
    R = Ri.mean()

    # residu = sum((Ri - R) ** 2)
    # print(residu)

    # convert the location of the center point back to global coordinates.
    xyz = local_to_world_coordinates_numpy(frame, [[c[0], c[1], 0.0]])[0]
    return xyz, uvw[2], R


def bestfit_sphere_numpy(points):
    """Returns the sphere's center and radius that fits best through a set of points.

    Parameters
    ----------
    points: array_like[point]
        XYZ coordinates of the points.

    Returns
    -------
    [float, float, float]
        Sphere center (XYZ coordinates).
    float
        Sphere radius.

    Notes
    -----
    For more information see [1]_.

    Examples
    --------
    >>> from compas.geometry import bestfit_sphere_numpy
    >>> points = [(291.580, -199.041, 120.194), (293.003, -52.379, 33.599),\
                  (514.217, 26.345, 29.143), (683.253, 26.510, -6.194),\
                  (683.247, -327.154, 179.113), (231.606, -430.659, 115.458),\
                  (87.278, -419.178, -18.863), (24.731, -340.222, -127.158)]
    >>> center, radius = bestfit_sphere_numpy(points)

    References
    ----------
    .. [1] Least Squares Sphere Fit.
           Available at: https://jekel.me/2015/Least-Squares-Sphere-Fit/.

    """

    # Assemble the A matrix
    spX = asarray([p[0] for p in points])
    spY = asarray([p[1] for p in points])
    spZ = asarray([p[2] for p in points])
    A = zeros((len(spX), 4))
    A[:, 0] = spX * 2
    A[:, 1] = spY * 2
    A[:, 2] = spZ * 2
    A[:, 3] = 1

    # Assemble the f matrix
    f = zeros((len(spX), 1))
    f[:, 0] = (spX * spX) + (spY * spY) + (spZ * spZ)
    C, residules, rank, singval = lstsq(A, f)

    # solve for the radius
    t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3]
    radius = sqrt(t)
    return [float(C[0][0]), float(C[1][0]), float(C[2][0])], radius


# src/compas/geometry/booleans/__init__.py

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

from compas.plugins import pluggable


@pluggable(category="booleans")
def boolean_union_mesh_mesh(A, B):
    """Compute the boolean union of two triangle meshes.

    Parameters
    ----------
    A : tuple[sequence[point], sequence[[int, int, int]]]
        The vertices and faces of mesh A.
    B : tuple[sequence[point], sequence[[int, int, int]]]
        The vertices and faces of mesh B.

    Returns
    -------
    tuple[list[point], list[[int, int, int]]]
        The vertices and the faces of the boolean union.

    Examples
    --------
    >>> from compas.geometry import Box, Sphere
    >>> from compas.geometry import boolean_union_mesh_mesh     # doctest: +SKIP
    >>> from compas.geometry import trimesh_remesh              # doctest: +SKIP
    >>> from compas.datastructures import Mesh

    >>> box = Box.from_width_height_depth(2, 2, 2)
    >>> box = Mesh.from_shape(box)
    >>> box.quads_to_triangles()

    >>> sphere = Sphere([1, 1, 1], 1)
    >>> sphere = Mesh.from_shape(sphere, u=30, v=30)
    >>> sphere.quads_to_triangles()

    >>> A = box.to_vertices_and_faces()
    >>> B = sphere.to_vertices_and_faces()
    >>> B = trimesh_remesh(B, 0.3, 10)                          # doctest: +SKIP

    >>> V, F = boolean_union_mesh_mesh(A, B)                    # doctest: +SKIP
    >>> union = Mesh.from_vertices_and_faces(V, F)              # doctest: +SKIP

    """
    raise NotImplementedError


@pluggable(category="booleans")
def boolean_difference_mesh_mesh(A, B):
    """Compute the boolean difference of two triangle meshes.

    Parameters
    ----------
    A : tuple[sequence[point], sequence[[int, int, int]]]
        The vertices and faces of mesh A.
    B : tuple[sequence[point], sequence[[int, int, int]]]
        The vertices and faces of mesh B.

    Returns
    -------
    tuple[list[point], list[[int, int, int]]]
        The vertices and the faces of the boolean difference.

    """
    raise NotImplementedError


@pluggable(category="booleans")
def boolean_intersection_mesh_mesh(A, B):
    """Compute the boolean intersection of two triangle meshes.

    Parameters
    ----------
    A : tuple[sequence[point], sequence[[int, int, int]]]
        The vertices and faces of mesh A.
    B : tuple[sequence[point], sequence[[int, int, int]]]
        The vertices and faces of mesh B.

    Returns
    -------
    tuple[list[point], list[[int, int, int]]]
        The vertices and the faces of the boolean intersection.

    """
    raise NotImplementedError


@pluggable(category="booleans")
def boolean_union_polygon_polygon(A, B):
    """Compute the boolean union of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[point]
        The vertices of the boolean union.

    """
    raise NotImplementedError


@pluggable(category="booleans")
def boolean_difference_polygon_polygon(A, B):
    """Compute the boolean difference of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[point]
        The vertices of the boolean difference.

    """
    raise NotImplementedError


@pluggable(category="booleans")
def boolean_symmetric_difference_polygon_polygon(A, B):
    """Compute the boolean symmetric difference of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[point]
        The vertices of the boolean symmetric difference.

    """
    raise NotImplementedError


@pluggable(category="booleans")
def boolean_intersection_polygon_polygon(A, B):
    """Compute the boolean intersection of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[point]
        The vertices of the boolean difference.

    """
    raise NotImplementedError


# src/compas/geometry/booleans/booleans_shapely.py

from compas.plugins import plugin
from shapely.geometry import Polygon


@plugin(category="booleans", requires=["shapely"])
def boolean_union_polygon_polygon(A, B):
    """Compute the boolean union of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[point]
        The vertices of the boolean union.

    """
    a = Polygon([point[:2] for point in A.points])
    b = Polygon([point[:2] for point in B.points])

    c = a.union(b)

    return list(c.exterior.coords)  # type: ignore


@plugin(category="booleans", requires=["shapely"])
def boolean_difference_polygon_polygon(A, B):
    """Compute the boolean differencen of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[point]
        The vertices of the boolean difference.

    """
    a = Polygon([point[:2] for point in A.points])
    b = Polygon([point[:2] for point in B.points])

    c = a.difference(b)

    return list(c.exterior.coords)  # type: ignore


@plugin(category="booleans", requires=["shapely"])
def boolean_symmetric_difference_polygon_polygon(A, B):
    """Compute the boolean symmetric difference of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[list[point]]
        The lists of vertices of the boolean symmetric difference.

    """
    a = Polygon([point[:2] for point in A.points])
    b = Polygon([point[:2] for point in B.points])

    c = a.symmetric_difference(b)

    return [list(polygon.exterior.coords) for polygon in c.geoms]  # type: ignore


@plugin(category="booleans", requires=["shapely"])
def boolean_intersection_polygon_polygon(A, B):
    """Compute the boolean intersection of two polygons.

    For this operation, the polygons are assumed to lie in the XY plane.
    Therefore, the Z components of the points defining the polygons are simply ignored.
    If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
    Otherwise the results are meaningless.

    Parameters
    ----------
    A : sequence[point]
        The vertices of polygon A.
    B : sequence[point]
        The vertices of polygon B.

    Returns
    -------
    list[point]
        The vertices of the boolean difference.

    """
    a = Polygon([point[:2] for point in A.points])
    b = Polygon([point[:2] for point in B.points])

    c = a.intersection(b)

    return list(c.exterior.coords)  # type: ignore


# src/compas/geometry/brep/__init__.py

class BrepError(Exception):
    """Represents a generic error in the Brep context"""

    pass


class BrepInvalidError(BrepError):
    """Raised when the process of re-constructing a Brep has resulted in an invalid Brep"""

    pass


class BrepTrimmingError(BrepError):
    """Raised when a trimming operation has failed or had not result"""

    pass


# src/compas/geometry/brep/brep.py

from compas.geometry import Geometry
from compas.plugins import pluggable
from compas.plugins import PluginNotInstalledError


LINEAR_DEFLECTION = 1e-3


@pluggable(category="factories")
def new_brep(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_native(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_mesh(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_box(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_cylinder(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_sphere(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_cone(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_surface(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_torus(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_sweep(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_step_file(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_polygons(*args, **kwargs):
    raise PluginNotInstalledError()


@pluggable(category="factories")
def from_curves(*args, **kwargs):
    raise PluginNotInstalledError()


class BrepType(object):
    """Possible types of a Brep

    Attributes
    ----------
    COMPOUND
    COMPSOLID
    SHELL
    FACE
    WIRE
    EDGE
    VERTEX
    SHAPE

    """

    COMPOUND = 0
    COMPSOLID = 1
    SHELL = 2
    FACE = 3
    WIRE = 4
    EDGE = 5
    VERTEX = 6
    SHAPE = 7


class BrepOrientation(object):
    """Possible orientations of a Brep

    Attributes
    ----------
    FORWARD
    REVERSED
    INTERNAL
    EXTERNAL

    """

    FORWARD = 0
    REVERSED = 1
    INTERNAL = 2
    EXTERNAL = 3


class Brep(Geometry):
    """Contains the topological and geometrical information of a Brep shape.

    This class serves as an interface for a Brep and allows instantiating a Brep object depending on the available Backend.
    Note: this is not a full implementation of Brep and rather relies on COMPAS's plugin system for actual implementation.

    Attributes
    ----------
    vertices : list[:class:`~compas.geometry.BrepVertex`], read-only
        The vertices of the Brep.
    edges : list[:class:`~compas.geometry.BrepEdge`], read-only
        The edges of the Brep.
    trims : list[:class:`~compas.geometry.BrepTrim`], read-only
        The trims of the Brep.
    loops : list[:class:`~compas.geometry.BrepLoop`], read-only
        The loops of the Brep.
    faces : list[:class:`~compas.geometry.BrepFace`], read-only
        The faces of the Brep.
    frame : :class:`~compas.geometry.Frame`, read-only
        The local coordinate system of the Brep.
    area : float, read-only
        The surface area of the Brep.
    volume : float, read-only
        The volume of the regions contained by the Brep.
    solids : list[:class:`~compas.geometry.Brep`], read-only
        The solids of this brep.
    shells : list[:class:`~compas.geometry.Brep`], read-only
        The shells of this brep.
    points : list[:class:`~compas.geometry.Point`], read-only
        The points of this brep.
    centroid : :class:`~compas.geometry.Point`, read-only
        The centroid of this brep.
    is_valid : bool, read-only
        True if this brep is valid, False otherwise
    is_solid : bool, read-only
        True if this brep is a solid, False otherwise.
    is_compound : bool, read-only
        True if this brep's type is a compound, False otherwise.
    is_compoundsolid : bool, read-only
        True if this brep's type is a compoundsolid, False otherwise.
    is_orientable : bool, read-only
        True if this brep is orientable, False otherwise.
    is_closed : bool, read-only
        True if this brep is closed, False otherwise.
    is_infinite : bool, read-only
        True if this brep is infinite, False otherwise.
    is_convex : bool, read-only
        True if this brep is convex, False otherwise.
    is_manifold : bool, read-only
        True if this brep is a manifold, False otherwise.
    is_surface : bool, read-only
        True if this brep is a surface, False otherwise.

    Other Attributes
    ----------------
    native_brep
        The underlying instance of the backend brep.
    type : :class:`~compas.geometry.BrepType`, read-only
        The type of Brep shape.
    orientation : :class:`~compas.geometry.BrepOrientation`, read-obly
        Orientation of the shape.

    """

    def __new__(cls, *args, **kwargs):
        return new_brep(cls, *args, **kwargs)

    def __init__(self, name=None):
        super(Brep, self).__init__(name=name)

    def __str__(self):
        lines = [
            "Brep",
            "-----",
            "Vertices: {}".format(len(self.vertices)),
            "Edges: {}".format(len(self.edges)),
            "Loops: {}".format(len(self.loops)),
            "Faces: {}".format(len(self.faces)),
            "Frame: {}".format(self.frame),
            "Area: {}".format(self.area),
            "Volume: {}".format(self.volume),
        ]
        return "\n".join(lines)

    # ==============================================================================
    # Data
    # ==============================================================================

    @property
    def DATASCHEMA(self):
        import schema

        return schema.Schema(
            {
                "faces": list,
            }
        )

    @property
    def data(self):
        faces = []
        for face in self.faces:
            faces.append(face.data)
        return {"faces": faces}

    @data.setter
    def data(self):
        raise NotImplementedError

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def native_brep(self):
        raise NotImplementedError

    @property
    def orientation(self):
        raise NotImplementedError

    @property
    def type(self):
        raise NotImplementedError

    @property
    def is_valid(self):
        raise NotImplementedError

    @property
    def is_shell(self):
        raise NotImplementedError

    @property
    def is_solid(self):
        raise NotImplementedError

    @property
    def is_compound(self):
        raise NotImplementedError

    @property
    def is_compoundsolid(self):
        raise NotImplementedError

    @property
    def is_orientable(self):
        raise NotImplementedError

    @property
    def is_closed(self):
        raise NotImplementedError

    @property
    def is_infinite(self):
        raise NotImplementedError

    @property
    def is_convex(self):
        raise NotImplementedError

    @property
    def is_manifold(self):
        raise NotImplementedError

    @property
    def is_surface(self):
        raise NotImplementedError

    # ==============================================================================
    # Geometric Components
    # ==============================================================================

    @property
    def points(self):
        raise NotImplementedError

    @property
    def curves(self):
        raise NotImplementedError

    @property
    def surfaces(self):
        raise NotImplementedError

    # ==============================================================================
    # Topological Components
    # ==============================================================================

    @property
    def vertices(self):
        raise NotImplementedError

    @property
    def edges(self):
        raise NotImplementedError

    @property
    def trims(self):
        raise NotImplementedError

    @property
    def loops(self):
        raise NotImplementedError

    @property
    def faces(self):
        raise NotImplementedError

    @property
    def shells(self):
        raise NotImplementedError

    @property
    def solids(self):
        raise NotImplementedError

    # ==============================================================================
    # Geometric Properties
    # ==============================================================================

    @property
    def frame(self):
        raise NotImplementedError

    @property
    def area(self):
        raise NotImplementedError

    @property
    def volume(self):
        raise NotImplementedError

    @property
    def centroid(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_native(cls, native_brep):
        """Creates a Brep from an instance of a native backend Brep type.

        Parameters
        ----------
        brep : an instance of a Brep from a supported Brep backend
            e.g. Rhino.Geometry.Brep

        Returns
        -------
        :class:`~compas.geometry.Brep`
        """
        return from_native(native_brep)

    @classmethod
    def from_step_file(cls, filename):
        """Conctruct a Brep from the data contained in a STEP file.

        Parameters
        ----------
        filename : str

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_step_file(filename)

    @classmethod
    def from_polygons(cls, polygons):
        """Construct a Brep from a set of polygons.

        Parameters
        ----------
        polygons : list[:class:`~compas.geometry.Polygon`]

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_polygons(polygons)

    @classmethod
    def from_curves(cls, curves):
        """Construct a Brep from a set of curves.

        Parameters
        ----------
        curves : list[:class:`~compas.geometry.NurbsCurve`]

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_curves(curves)

    @classmethod
    def from_box(cls, box):
        """Construct a Brep from a COMPAS box.

        Parameters
        ----------
        box : :class:`~compas.geometry.Box`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_box(box)

    @classmethod
    def from_sphere(cls, sphere):
        """Construct a Brep from a COMPAS sphere.

        Parameters
        ----------
        sphere : :class:`~compas.geometry.Sphere`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_sphere(sphere)

    @classmethod
    def from_cylinder(cls, cylinder):
        """Construct a Brep from a COMPAS cylinder.

        Parameters
        ----------
        cylinder : :class:`~compas.geometry.Cylinder`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_cylinder(cylinder)

    @classmethod
    def from_cone(cls, cone):
        """Construct a Brep from a COMPAS cone.

        Parameters
        ----------
        cone : :class:`~compas.geometry.Cone`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_cone(cone)

    @classmethod
    def from_torus(cls, torus):
        """Construct a Brep from a COMPAS torus.

        Parameters
        ----------
        torus : :class:`~compas.geometry.Torus`

        Returns
        -------
        :class:`~compas.geometry.BRep`

        """
        return from_torus(torus)

    @classmethod
    def from_mesh(cls, mesh):
        """Construct a Brep from a COMPAS mesh.

        Parameters
        ----------
        mesh : :class:`~compas.datastructures.Mesh`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        return from_mesh(mesh)

    @classmethod
    def from_brepfaces(cls, faces):
        """Make a Brep from a list of Brep faces forming an open or closed shell.

        Parameters
        ----------
        faces : list[:class:`~compas.geometry.BrepFace`]

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        raise NotImplementedError

    @classmethod
    def from_extrusion(cls, curve, vector):
        """Construct a Brep by extruding a closed curve along a direction vector.

        Parameters
        ----------
        curve : :class:`~compas.geometry.Curve`
            The curve to extrude
        vector : :class:`~compas.geometry.Vector`
            The vector to extrude the curve by

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        raise NotImplementedError

    @classmethod
    def from_sweep(cls, profile, path):
        """Construct a BRep by sweeping a profile along a path.

        Parameters
        ----------
        profile : :class:`~compas.geometry.BrepEdge` or :class:`~compas.geometry.BrepFace`
            the profile to sweep. Either an edge or a face.
        path : :class:`~compas.geometry.BrepLoop`
            the path to sweep along

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        raise NotImplementedError

    # ==============================================================================
    # Boolean Constructors
    # ==============================================================================

    @classmethod
    def from_boolean_difference(cls, brep_a, brep_b):
        """Construct a Brep from the boolean difference of two other Breps.

        Parameters
        ----------
        brep_a : :class:`~compas.geometry.Brep`
        brep_b : :class:`~compas.geometry.Brep`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        raise NotImplementedError

    @classmethod
    def from_boolean_intersection(cls, brep_a, brep_b):
        """Construct a BRep from the boolean intersection of two other Breps.

        Parameters
        ----------
        brep_a : :class:`~compas.geometry.Brep`
        brep_b : :class:`~compas.geometry.Brep`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        raise NotImplementedError

    @classmethod
    def from_boolean_union(cls, brep_a, brep_b):
        """Construct a Brep from the boolean union of two other Breps.

        Parameters
        ----------
        brep_a : :class:`~compas.geometry.Brep`
        brep_b : :class:`~compas.geometry.Brep`

        Returns
        -------
        :class:`~compas.geometry.Brep`

        """
        raise NotImplementedError

    def __sub__(self, other):
        """Compute the boolean difference using the "-" operator of this shape and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Brep`
            The other Brep to create a union with.

        Returns
        -------
        :class:`~compas.geometry.Brep`
            The Brep resulting from the difference operation.

        """
        results = type(self).from_boolean_difference(self, other)
        if isinstance(results, list):
            results = results[0]
        return results

    def __and__(self, other):
        """Compute the boolean intersection using the "&" operator of this shape and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Brep`
            The other Brep to create a union with.

        Returns
        -------
        :class:`~compas.geometry.Brep`
            The Brep resulting from the intersection operation.

        """
        results = type(self).from_boolean_intersection(self, other)
        if isinstance(results, list):
            results = results[0]
        return results

    def __add__(self, other):
        """Compute the boolean union using the "+" operator of this Brep and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Brep`
            The other Brep to create a union with.

        Returns
        -------
        :class:`~compas.geometry.Brep`
            The Brep resulting from the union operation.

        """
        results = type(self).from_boolean_union(self, other)
        if isinstance(results, list):
            results = results[0]
        return results

    # ==============================================================================
    # Converters
    # ==============================================================================

    def to_json(self, filepath):
        """Export the BRep to a JSON file.

        Parameters
        ----------
        filepath : str
            Location of the file.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def to_step(self, filepath):
        """Write the BRep shape to a STEP file.

        Parameters
        ----------
        filepath : str
            Location of the file.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def to_tesselation(self, linear_deflection=LINEAR_DEFLECTION):
        """Create a tesselation of the shape for visualisation.

        Parameters
        ----------
        linear_deflection : float, optional
            Allowable deviation between curved geometry and mesh discretisation.

        Returns
        -------
        :class:`~compas.datastructures.Mesh`

        """
        raise NotImplementedError

    def to_meshes(self, u=16, v=16):
        """Convert the faces of this Brep shape to meshes.

        Parameters
        ----------
        u : int, optional
            The number of mesh faces in the U direction of the underlying surface geometry of every face of the Brep.
        v : int, optional
            The number of mesh faces in the V direction of the underlying surface geometry of every face of the Brep.

        Returns
        -------
        list[:class:`~compas.datastructures.Mesh`]

        """
        raise NotImplementedError

    def to_viewmesh(self, precision):
        """Convert this Brep to a view mesh.

        Parameters
        ----------
        precision : float
            The presicion by which the mesh is estimated

        Returns
        -------
        :class:`~compas.datastructure.Mesh`

        """
        raise NotImplementedError

    # ==============================================================================
    # Relationships
    # ==============================================================================

    def vertex_neighbors(self, vertex):
        """Identify the neighbouring vertices of a given vertex.

        Parameters
        ----------
        vertex : :class:`~compas.geometry.BrepVertex`

        Returns
        -------
        list[:class:`~compas.geometry.BrepVertex`]

        """
        raise NotImplementedError

    def vertex_edges(self, vertex):
        """Identify the edges connected to a given vertex.

        Parameters
        ----------
        vertex : :class:`~compas.geometry.BrepVertex`

        Returns
        -------
        list[:class:`~compas.geometry.BrepEdge`]

        """
        raise NotImplementedError

    def vertex_faces(self, vertex):
        """Identify the faces connected to a vertex.

        Parameters
        ----------
        vertex : :class:`~compas.geometry.BrepVertex`

        Returns
        -------
        list[:class:`~compas.geometry.BrepFace`]

        """
        raise NotImplementedError

    # ==============================================================================
    # Other Methods
    # ==============================================================================

    def trim(self, trimming_plane, tolerance):
        """Trim this Brep using the given trimming plane.

        Parameters
        ----------
        trimming_plane : :class:`~compas.geometry.Frame`
            defines the trimming plane
        tolerance: float
            the tolerance to use when trimming

        Returns
        -------
        None

        """
        raise NotImplementedError

    def make_solid(self):
        """Convert the current shape to a solid if it is a shell.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def sew(self):
        """Sew together the individual parts of the shape.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def fix(self):
        """Fix the shell.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def cull_unused_vertices(self):
        """Remove all unused vertices.

        Returns
        -------
        None

        """
        NotImplementedError

    def cull_unused_edges(self):
        """Remove all unused edges.

        Returns
        -------
        None

        """
        NotImplementedError

    def cull_unused_loops(self):
        """Remove all unused loops.

        Returns
        -------
        None

        """
        NotImplementedError

    def cull_unused_faces(self):
        """Remove all unused faces.

        Returns
        -------
        None

        """
        NotImplementedError

    def contours(self, planes):
        """Generate contour lines by slicing the Brep shape with a series of planes.

        Parameters
        ----------
        planes : list[:class:`~compas.geometry.Plane`]
            The slicing planes.

        Returns
        -------
        list[list[:class:`~compas.geometry.Polyline`]]
            A list of polylines per plane.

        """
        raise NotImplementedError

    def slice(self, plane):
        """Slice through the BRep with a plane.

        Parameters
        ----------
        plane : :class:`compas.geometry.Plane`

        Returns
        -------
        :class:`~compas.geometry.BrepFace`

        """

    def split(self, cutter):
        """Slice through the BRep with a plane.

        Parameters
        ----------
        cutter : :class:`~compas.geomtery.Brep`
            Another Brep to use as a cutter.

        Returns
        -------
        list[:class:`~compas.geometry.Brep`]

        """
        raise NotImplementedError

    def overlap(self, other, deflection=LINEAR_DEFLECTION, tolerance=0.0):
        """Compute the overlap between this BRep and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Brep`
            The other Brep.
        deflection : float, optional
            Allowable deflection for mesh generation used for proximity detection.
        tolerance : float, optional
            Tolerance for overlap calculation.

        Returns
        -------
        tuple[list[:class:`~compas.geometry.BrepFace`]]

        """
        raise NotImplementedError


# src/compas/geometry/brep/edge.py

from compas.data import Data


class BrepEdge(Data):
    """An interface for a Brep Edge

    Attributes
    ----------
    is_line : bool, read-only
        Returns True if this edge is a line, False otherwise.
    is_circle : bool, read-only
        Returns True if this edge is a circle, False otherwise.
    is_ellipse : bool, read-only
        Returns True if this edge is an ellipse, False otherwise.
    is_hyperbola : bool, read-only
        Returns True if this edge is a hyperbola, False otherwise.
    is_parabola : bool, read-only
        Returns True if this edge is a parabola, False otherwise.
    is_bezier : bool, read-only
        Returns True if this edge is a bezier, False otherwise.
    is_bspline : bool, read-only
        Returns True if this edge is a bspline, False otherwise.
    is_other : bool, read-only
        Returns True if this edge is of another shape, False otherwise.
    is_valid : bool, read-only
        Returns True if this edge is valid, False otherwise.
    vertices : list[:class:`~compas.geometry.BrepVertex`], read-only
        Gets the list of vertices which compound this edge.
    first_vertex : :class:`~compas.geometry.BrepVertex`
        Returns the first vertex of this edge.
    last_vertex : :class:`~compas.geometry.BrepVertex`
        Returns the last vertex of this edge.
    curve : :class:`~compas.geometry.Curve`
        Returns the curve geometry of this edge.

    """

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def is_line(self):
        raise NotImplementedError

    @property
    def is_circle(self):
        raise NotImplementedError

    @property
    def is_ellipse(self):
        raise NotImplementedError

    @property
    def is_hyperbola(self):
        raise NotImplementedError

    @property
    def is_parabola(self):
        raise NotImplementedError

    @property
    def is_bezier(self):
        raise NotImplementedError

    @property
    def is_bspline(self):
        raise NotImplementedError

    @property
    def is_other(self):
        raise NotImplementedError

    @property
    def is_valid(self):
        raise NotImplementedError

    @property
    def vertices(self):
        raise NotImplementedError

    @property
    def first_vertex(self):
        raise NotImplementedError

    @property
    def last_vertex(self):
        raise NotImplementedError

    @property
    def curve(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_vertices(cls, vertices):
        """Construct an edge from two vertices.

        Parameters
        ----------
        vertices : list[:class:`~compas.geometry.BrepVertex`]

        Returns
        -------
        :class:`~compas.geometry.BrepVertex`

        """
        raise NotImplementedError

    @classmethod
    def from_points(cls, points):
        """Construct an edge from two points.

        Parameters
        ----------
        points : list[:class:`~compas.geometry.Point`]

        Returns
        -------
        :class:`~compas.geometry.BrepVertex`

        """
        raise NotImplementedError

    @classmethod
    def from_line(cls, line):
        """Construct an edge from a line.

        Parameters
        ----------
        line : :class:`~compas.geometry.Line`

        Returns
        -------
        :class:`~compas.geometry.BrepEdge`

        """
        raise NotImplementedError

    @classmethod
    def from_circle(cls, circle):
        """Construct an edge from a circle.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`

        Returns
        -------
        :class:`~compas.geometry.BrepEdge`

        """
        raise NotImplementedError

    @classmethod
    def from_ellipse(cls, ellipse):
        """Construct an edge from an ellipse.

        Parameters
        ----------
        ellipse : :class:`~compas.geometry.Ellipse`

        Returns
        -------
        :class:`~compas.geometry.BrepEdge`

        """
        raise NotImplementedError

    @classmethod
    def from_curve(cls, curve):
        """Construct an edge from a curve.

        Parameters
        ----------
        curve : :class:`~compas.geometry.Curve`

        Returns
        -------
        :class:`~compas.geometry.BrepEdge`

        """
        raise NotImplementedError

    # ==============================================================================
    # Conversions
    # ==============================================================================

    def to_line(self):
        """Get a line from this edge's geometry.

        Returns
        -------
        :class:`~compas.geometry.Line`

        """
        raise NotImplementedError

    def to_circle(self):
        """Get a circle from this edge's geometry.

        Returns
        -------
        :class:`~compas.geometry.Circle`

        """
        raise NotImplementedError

    def to_ellipse(self):
        """Get an ellipse from this edge's geometry.

        Returns
        -------
        :class:`~compas.geometry.Ellipse`

        """
        raise NotImplementedError

    def to_hyperbola(self):
        """Get a hyperbola from this edge's geometry.

        Returns
        -------
        TODO: type?

        """
        raise NotImplementedError

    def to_parabola(self):
        """Get a parabola from this edge's geometry.

        Returns
        -------
        TODO: type?

        """
        raise NotImplementedError

    def to_bezier(self):
        """Get a bezier from this edge's geometry.

        Returns
        -------
        :class:`~compas.geometry.Bezier`

        """
        raise NotImplementedError

    def to_bspline(self):
        """Get a bspline from this edge's geometry.

        Returns
        -------
        TODO: type?

        """
        raise NotImplementedError

    def to_curve(self):
        """Get a curve from this edge's geometry.

        Returns
        -------
        :class:`~compas.geometry.Curve`

        """
        raise NotImplementedError


# src/compas/geometry/brep/face.py

from compas.data import Data


class BrepFace(Data):
    """An interface for a Brep Face.

    Attributes
    ----------
    is_plane : bool, read-only
        Returns True if this face is a plane, False otherwise.
    is_cylinder : bool, read-only
        Returns True if this face is a cylinder, False otherwise.
    is_sphere : bool, read-only
        Returns True if this face is a sphere, False otherwise.
    is_torus : bool, read-only
        Returns True if this face is a torus, False otherwise.
    is_cone : bool, read-only
        Returns True if this face is a cone, False otherwise.
    is_bspline : bool, read-only
        Returns True if this face is a bspline, False otherwise.
    vertices : list[:class:`~compas.geometry.BrepVertex`], read-only
        Returns a list of the vertices comprising this face.
    edges : list[:class:`~compas.geometry.BrepEdge`], read-only
        Returns a list of the edges comprising this face.
    loops : list[:class:`~compas.geometry.BrepLoop`], read-only
        Returns a list of the loops comprising this face.
    surface : :class:`~compas.geometry.Surface`, read-only
        Returns the geometry of this face as a surface.
    nurbssurface : :class:`~compas.geometry.NurbsSurface`, read-only
        Returns the geometry of this face as a NURBS surface.
    area : float, read-only
        Returns the area of this face's geometry.
    centroid : :class:`~compas.geometry.Point`, read-only
        Returns the centroid of this face's geometry.
    is_valid : bool, read-only
        Return True if this face is valid, False otherwise.

    """

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def is_plane(self):
        raise NotImplementedError

    @property
    def is_cylinder(self):
        raise NotImplementedError

    @property
    def is_sphere(self):

        raise NotImplementedError

    @property
    def is_torus(self):
        raise NotImplementedError

    @property
    def is_cone(self):
        raise NotImplementedError

    @property
    def is_bspline(self):
        raise NotImplementedError

    @property
    def vertices(self):
        raise NotImplementedError

    @property
    def edges(self):
        raise NotImplementedError

    @property
    def loops(self):
        raise NotImplementedError

    @property
    def surface(self):
        raise NotImplementedError

    @property
    def nurbssurface(self):
        raise NotImplementedError

    @property
    def area(self):
        raise NotImplementedError

    @property
    def centroid(self):
        raise NotImplementedError

    @property
    def is_valid(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_plane(cls, plane):
        """Construct a face from a plane geometry.

        Parameters
        ----------
        plane : :class:`~compas.geometry.Plane`

        Returns
        -------
        :class:`~compas.geometry.BrepFace`

        """
        raise NotImplementedError

    @classmethod
    def from_cylinder(cls):
        """Construct a face from a cylinder geometry.

        Parameters
        ----------
        cylinder : :class:`~compas.geometry.Cylinder`

        Returns
        -------
        :class:`~compas.geometry.BrepFace`

        """
        raise NotImplementedError

    @classmethod
    def from_cone(cls, cone):
        """Construct a face from a cone geometry.

        Parameters
        ----------
        cone : :class:`~compas.geometry.Cone`

        Returns
        -------
        :class:`~compas.geometry.BrepFace`

        """
        raise NotImplementedError

    @classmethod
    def from_sphere(cls, sphere):
        """Construct a face from a sphere geometry.

        Parameters
        ----------
        sphere : :class:`~compas.geometry.Sphere`

        Returns
        -------
        :class:`~compas.geometry.BrepFace`

        """
        raise NotImplementedError

    @classmethod
    def from_torus(cls, torus):
        """Construct a face from a torus geometry.

        Parameters
        ----------
        torus : :class:`~compas.geometry.Torus`

        Returns
        -------
        :class:`~compas.geometry.BrepFace`

        """
        raise NotImplementedError

    @classmethod
    def from_surface(cls, surface):
        """Construct a face from a surfaces geometry.

        Parameters
        ----------
        surface : :class:`~compas.geometry.Surface`

        Returns
        -------
        :class:`~compas.geometry.BrepFace`

        """
        raise NotImplementedError


# src/compas/geometry/brep/loop.py

from compas.data import Data


class BrepLoop(Data):
    """An interface for a Brep Loop

    Attributes
    ----------
    is_valid : bool, read-only
        Returns True if this loop is valid, False otherwise.
    vertices : list[:class:`~compas.geometry.BrepVertex`], read-only
        Returns the list of vertices associated with this loop.
    edges : list[:class:`~compas.geometry.BrepEdge`], read-only
        Returns the list of deges associated with this loop.

    """

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def is_valid(self):
        return NotImplementedError

    @property
    def vertices(self):
        raise NotImplementedError

    @property
    def edges(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_edges(cls, edges):
        """Construct a loop from a list of edges.

        Parameters
        ----------
        edges : list[:class:`~compas.geometry.BrepEdge`]

        Returns
        -------
        :class:`~compas.geometry.BrepLoop`

        """
        raise NotImplementedError

    @classmethod
    def from_polyline(cls, polyline):
        """Construct a loop from a polyline.

        Parameters
        ----------
        polyline : :class:`~compas.geometry.Polyline`

        Returns
        -------
        :class:`~compas.geometry.BrepLoop`

        """
        raise NotImplementedError

    @classmethod
    def from_polygon(cls, polygon):
        """Construct a loop from a polygon.

        Parameters
        ----------
        polygon : :class:`~compas.geometry.Polygon`

        Returns
        -------
        :class:`~compas.geometry.BrepLoop`

        """
        raise NotImplementedError


# src/compas/geometry/brep/trim.py

from compas.data import Data


class BrepTrimIsoStatus(object):
    """An enumeration of isoparametric curve direction on the surface."""

    NONE = 0
    X = 1
    Y = 2
    WEST = 3
    SOUTH = 4
    EAST = 5
    NORTH = 6


class BrepTrim(Data):
    """An interface for a Brep Trim

    Attributes
    ----------
    curve : :class:`~compas.geometry.NurbsCurve`, read_only
        Returns the geometry for this trim as a 2d curve.
    iso_status : literal(NONE|X|Y|WEST|SOUTH|EAST|NORTH)
        The isoparametric curve direction on the surface.
    is_reversed : bool
        True if this trim is reversed from its associated edge curve and False otherwise.

    """

    @property
    def curve(self):
        raise NotImplementedError

    @property
    def iso_status(self):
        raise NotImplementedError

    @property
    def is_reversed(self):
        raise NotImplementedError


# src/compas/geometry/brep/vertex.py

from compas.data import Data


class BrepVertex(Data):
    """An interface for a Brep Vertex

    Attributes
    ----------
    point : :class:`~compas.geometry.Point`, read_only
        Returns the geometry of this vertex as a 3D point

    """

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def point(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_point(cls, point):
        """Creates a Brep vertex from a Point

        Parameters
        ----------
        point: :class:`~compas.geometry.Point`
            The point to create a vertex from

        Returns
        -------
        :class:`~compas.geometry.BrepVertex`
            The created vertex
        """
        raise NotImplementedError


# src/compas/geometry/curves/arc.py

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

import math

from compas.geometry import close
from compas.geometry import Frame
from compas.geometry import Circle
from .curve import Curve

PI2 = 2.0 * math.pi


class Arc(Curve):
    """An arc is a segment of a circle, and is defined by a coordinate system, radius, and start and end angles.

    The centre of the underlying circle is at the origin of the coordinate system.
    The start and end angles are measured from the positive X axis towards the positive Y axis.
    The parameter domain of the arc is defined by the start and end angles.
    It is a subset of the domain of the circle.

    Transformations of the arc are performed by transforming the local coordinate system.
    They are limited to (combinations of) translations and rotations.
    All other components of transformations are ignored.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`
        Coordinate frame at the center of the arc's circle.
    radius : float
        Radius of the arc's circle.
    start_angle : float, optional
        The angle in radians of the start of this Arc.
        Defaults to 0.0.
    end_angle : float, optional
        The angle in radians of the end of this Arc.
        Defaults to `pi` (180 degrees).

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The coordinate frame of the arc.
    radius : float
        The radius of the circle.
    start_angle : float
        The start angle of the arc.
    end_angle : float
        The end angle of the arc.
    circle : :class:`~compas.geometry.Circle`
        The underlying circle.
    center : :class:`~compas.geometry.Point`
        The center of the underlying circle.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the arc.
    normal : :class:`~compas.geometry.Vector`, read-only
        The normal of the arc plane.
    diameter : float, read-only
        The diameter of the underlying circle.
    length : float
        The length of the arc.
    angle : float, read-only
        The sweep angle in radians between start angle and end angle.
    circumference : float, read-only
        The circumference of the underlying circle.
    domain : tuple(float, float), read-only
        The parameter domain of the arc.
    start : :class:`~compas.geometry.Point`, read-only
        The start point of the arc.
    end : :class:`~compas.geometry.Point`, read-only
        The end point of the arc.
    is_closed : bool, read-only
        ``True`` if the arc is closed.
    transformation : :class:`Transformation`, read-only
        The transformation from the local coordinate system of the arc (:attr:`frame`) to the world coordinate system.

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "radius": {"type": "number", "minimum": 0},
            "start_angle": {"type": "number", "minimum": 0, "optional": True},
            "end_angle": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "radius", "start_angle", "end_angle"],
    }

    # overwriting the __new__ method is necessary
    # to avoid triggering the plugin mechanism of the base curve class
    def __new__(cls, *args, **kwargs):
        curve = object.__new__(cls)
        curve.__init__(*args, **kwargs)
        return curve

    def __init__(self, frame=None, radius=None, start_angle=0.0, end_angle=math.pi, **kwargs):
        super(Arc, self).__init__(frame=frame, **kwargs)

        self._radius = None
        self._start_angle = None
        self._end_angle = None

        self.radius = radius
        self.start_angle = start_angle
        self.end_angle = end_angle

    def __repr__(self):
        return "Arc({0!r}, {1!r}, {2!r}, {3!r})".format(
            self.frame,
            self.radius,
            self.start_angle,
            self.end_angle,
        )

    def __len__(self):
        return 4

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.radius
        elif key == 2:
            return self.start_angle
        elif key == 3:
            return self.end_angle
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.radius = value
        elif key == 2:
            self.start_angle = value
        elif key == 3:
            self.end_angle = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.radius, self.start_angle, self.end_angle])

    def __eq__(self, other):
        try:
            other_frame = other[0]
            other_radius = other[1]
            other_start = other[2]
            other_end = other[3]
        except Exception:
            return False
        return (
            self.frame == other_frame
            and self.radius == other_radius
            and self.start_angle == other_start
            and self.end_angle == other_end
        )

    # =============================================================================
    # Data
    # =============================================================================

    @property
    def data(self):
        return {
            "frame": self._frame,
            "radius": self._radius,
            "start_angle": self._start_angle,
            "end_angle": self._end_angle,
        }

    @data.setter
    def data(self, value):
        self.frame = value["frame"]
        self.radius = value["radius"]
        self.start_angle = value["start_angle"]
        self.end_angle = value["end_angle"]

    # =============================================================================
    # Properties
    # =============================================================================

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("Radius not set.")
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0.0:
            raise ValueError("Radius must be greater than or equal to zero.")
        self._radius = value

    @property
    def start_angle(self):
        if self._start_angle is None:
            self._start_angle = 0.0
        return self._start_angle

    @start_angle.setter
    def start_angle(self, value):
        if value < 0.0 or value > PI2:
            raise ValueError("Start angle must satisfy 0 <= angle <= 2 * pi.")
        self._start_angle = value

    @property
    def end_angle(self):
        if self._end_angle is None:
            raise ValueError("End angle not set.")
        return self._end_angle

    @end_angle.setter
    def end_angle(self, value):
        if value < 0.0 or value > PI2:
            raise ValueError("End angle must satisfy 0 <= angle <= 2 * pi.")
        self._end_angle = value

    @property
    def circle(self):
        return Circle(self.frame, self.radius)

    @property
    def length(self):
        return self.radius * self.angle

    @property
    def angle(self):
        return self.end_angle - self.start_angle

    # @property
    # def domain(self):
    #     return self.start_angle, self.end_angle

    @property
    def center(self):
        return self.frame.point

    @property
    def diameter(self):
        return 2.0 * self.radius

    @property
    def circumference(self):
        return self.diameter * math.pi

    @property
    def is_circle(self):
        return close(abs(abs(self.angle) - PI2), 0.0)

    @property
    def is_closed(self):
        return False

    @property
    def is_periodic(self):
        return False

    def _verify(self):
        if self.angle < 0.0 or self.angle > PI2:
            raise ValueError("Sweep angle must satisfy 0 < angle < 2 * Pi. Currently:{}".format(self.angle))

    # =============================================================================
    # Constructors
    # =============================================================================

    @classmethod
    def from_circle(cls, circle, start_angle, end_angle):
        """Creates an Arc from a circle and start and end angles.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`
            The frame and radius of this circle will be used to create an Arc.
        start_angle : float
            The start angle in radians.
        end_angle : float
            The end angle in radians.

        Returns
        -------
        :class:`~compas.geometry.Arc`

        """
        return cls(
            frame=circle.frame,
            radius=circle.radius,
            start_angle=start_angle,
            end_angle=end_angle,
        )

    # =============================================================================
    # Methods
    # =============================================================================


# src/compas/geometry/curves/bezier.py

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

from math import factorial

from compas.geometry import Vector
from compas.geometry import Point

from .curve import Curve


def binomial_coefficient(n, k):
    """Returns the binomial coefficient of the :math:`x^k` term in the
    polynomial expansion of the binomial power :math:`(1 + x)^n`.

    Parameters
    ----------
    n : int
        The number of terms.
    k : int
        The index of the coefficient.

    Returns
    -------
    int
        The coefficient.

    Notes
    -----
    Arranging binomial coefficients into rows for successive values of `n`,
    and in which `k` ranges from 0 to `n`, gives a triangular array known as
    Pascal's triangle.

    """
    return int(factorial(n) / float(factorial(k) * factorial(n - k)))


def bernstein(n, k, t):
    """k:sup:`th` of `n` + 1 Bernstein basis polynomials of degree `n`. A
    weighted linear combination of these basis polynomials is called a Bernstein
    polynomial.

    Parameters
    ----------
    n : int
        The degree of the polynomial.
    k : int
        The number of the basis polynomial.
    t : float
        The variable.

    Returns
    -------
    float
        The value of the Bernstein basis polynomial at `t`.

    Notes
    -----
    When constructing Bezier curves, the weights are simply the coordinates
    of the control points of the curve.

    References
    ----------
    More info at [1]_.

    .. [1] https://en.wikipedia.org/wiki/Bernstein_polynomial

    Examples
    --------
    >>> bernstein(3, 2, 0.5)
    0.375

    """
    if k < 0:
        return 0
    if k > n:
        return 0
    return binomial_coefficient(n, k) * t**k * (1 - t) ** (n - k)


class Bezier(Curve):
    """A Bezier curve is defined by control points and a degree.

    A Bezier curve of degree `n` is a linear combination of ``n + 1`` Bernstein
    basis polynomials of degree `n`.

    Parameters
    ----------
    points : sequence[point]
        A sequence of control points, represented by their location in 3D space.

    Attributes
    ----------
    points : list[:class:`~compas.geometry.Point`]
        The control points.
    degree : int, read-only
        The degree of the curve.

    Examples
    --------
    >>> curve = Bezier([[0.0, 0.0, 0.0], [0.5, 1.0, 0.0], [1.0, 0.0, 0.0]])
    >>> curve.degree
    2

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA},
        },
        "required": ["points"],
    }

    __slots__ = ["_points"]

    def __init__(self, points):
        super(Bezier, self).__init__()
        self._points = []
        self.points = points

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        """dict : The data dictionary that represents the curve."""
        return {"points": self.points}

    @data.setter
    def data(self, data):
        self.points = data["points"]

    @classmethod
    def from_data(cls, data):
        """Construct a curve from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Bezier`
            The constructed bezier curve.

        Examples
        --------
        >>> from compas.geometry import Bezier
        >>> data = {'points': [[0.0, 0.0, 0.0], [0.5, 1.0, 0.0], [1.0, 0.0, 0.0]]}
        >>> curve = Bezier.from_data(data)

        """
        return cls(data["points"])

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def points(self):
        return self._points

    @points.setter
    def points(self, points):
        if points:
            self._points = [Point(*point) for point in points]

    @property
    def degree(self):
        return len(self.points) - 1

    # ==========================================================================
    # constructors
    # ==========================================================================

    # ==========================================================================
    # methods
    # ==========================================================================

    def transform(self, T):
        """Transform this curve.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation`
            The transformation.

        Returns
        -------
        None

        """
        for point in self.points:
            point.transform(T)

    def point(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.

        Examples
        --------
        >>> curve = Bezier([[0.0, 0.0, 0.0], [0.5, 1.0, 0.0], [1.0, 0.0, 0.0]])
        >>> curve.point(0.0)
        Point(0.000, 0.000, 0.000)
        >>> curve.point(1.0)
        Point(1.000, 0.000, 0.000)

        """
        n = self.degree
        point = Point(0, 0, 0)
        for i, p in enumerate(self.points):
            b = bernstein(n, i, t)
            point += p * b
        return point

    def tangent(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.

        Examples
        --------
        >>> curve = Bezier([[0.0, 0.0, 0.0], [0.5, 1.0, 0.0], [1.0, 0.0, 0.0]])
        >>> curve.tangent(0.5)
        Vector(1.000, 0.000, 0.000)

        """
        n = self.degree
        v = Vector(0, 0, 0)
        for i, p in enumerate(self.points):
            a = bernstein(n - 1, i - 1, t)
            b = bernstein(n - 1, i, t)
            c = n * (a - b)
            v += p * c
        v.unitize()
        return v

    def locus(self, resolution=100):
        """Compute the locus of all points on the curve.

        Parameters
        ----------
        resolution : int
            The number of intervals at which a point on the curve should be computed.

        Returns
        -------
        list[:class:`~compas.geometry.Point`]
            Points along the curve.

        Examples
        --------
        >>> curve = Bezier([[0.0, 0.0, 0.0], [0.5, 1.0, 0.0], [1.0, 0.0, 0.0]])
        >>> points = curve.locus(10)
        >>> len(points) == 10
        True
        >>> points[0]
        Point(0.000, 0.000, 0.000)
        >>> points[-1]
        Point(1.000, 0.000, 0.000)

        """
        locus = []
        divisor = float(resolution - 1)
        for i in range(resolution):
            t = i / divisor
            locus.append(self.point(t))
        return locus


# src/compas/geometry/curves/circle.py

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

from math import pi, cos, sin

from compas.geometry import Frame
from compas.geometry import Plane
from .conic import Conic

PI2 = 2 * pi


class Circle(Conic):
    """A circle is a curve defined by a coordinate system and a radius.

    The centre of the circle is at the origin of the coordinate system.
    The z-axis of the coordinate system defines the normal of the circle plane.
    The parameter domain of a circle is ``[0, 2*pi]``.
    Moving along the circle in the parameter direction corresponds to moving counter-clockwise around the origin of the local coordinate system.

    Transformations of the circle are performed by transforming the coordinate system.
    They are limited to (combinations of) translations and rotations.
    All other components of transformations are ignored.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`, optional
        The coordinate frame of the circle.
        The default value is ``None``, in which case the world coordinate system is used.
    radius : float
        The radius of the circle.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The coordinate frame of the circle.
    radius : float
        The radius of the circle.
    center : :class:`~compas.geometry.Point`
        The center of the circle.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the circle.
    normal : :class:`~compas.geometry.Vector`, read-only
        The normal of the circle.
    area : float, read-only
        The area of the circle.
    circumference : float, read-only
        The circumference of the circle.
    domain : tuple(float, float), read-only
        The parameter domain of the circle.
    start : :class:`~compas.geometry.Point`, read-only
        The start point of the circle.
    end : :class:`~compas.geometry.Point`, read-only
        The end point of the circle.
    transformation : :class:`Transformation`, read-only
        The transformation from the local coordinate system of the circle (:attr:`frame`) to the world coordinate system.

    See Also
    --------
    :class:`compas.geometry.Ellipse`, :class:`compas.geometry.Arc`

    Examples
    --------
    >>> from compas.geometry import Frame, Circle
    >>> circle = Circle(frame=Frame.worldXY(), radius=5)
    >>> circle = Circle(radius=5)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "radius": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "radius"],
    }

    def __init__(self, frame=None, radius=1.0, **kwargs):
        super(Circle, self).__init__(frame=frame, **kwargs)
        self._radius = None
        self.radius = radius

    def __repr__(self):
        return "Circle({0!r}, {1!r})".format(self.frame, self.radius)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.radius
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.radius = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.radius])

    def __eq__(self, other):
        try:
            other_frame = other[0]
            other_radius = other[1]
        except Exception:
            return False
        return self.frame == other_frame and self.radius == other_radius

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "radius": self.radius}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.radius = data["radius"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def center(self):
        return self.frame.point

    @center.setter
    def center(self, point):
        self.frame.point = point

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("The radius of the circle has not been set yet.")
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("The radius of a circle should be larger than or equal to zero.")
        self._radius = float(radius)

    @property
    def area(self):
        return pi * (self.radius**2)

    @property
    def circumference(self):
        return 2 * pi * self.radius

    @property
    def eccentricity(self):
        return 0

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_point_and_radius(cls, point, radius):
        """Construct a circle from a point and a radius.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The center of the circle.
        radius : float
            The radius of the circle.

        Returns
        -------
        :class:`~compas.geometry.Circle`
            The constructed circle.

        """
        frame = Frame(point, [1, 0, 0], [0, 1, 0])
        return cls(frame=frame, radius=radius)

    @classmethod
    def from_plane_and_radius(cls, plane, radius):
        """Construct a circle from a plane and a radius.

        Parameters
        ----------
        plane : :class:`~compas.geometry.Plane`
            The plane of the circle.
        radius : float
            The radius of the circle.

        Returns
        -------
        :class:`~compas.geometry.Circle`
            The constructed circle.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> from compas.geometry import Circle
        >>> plane = Plane([0, 0, 0], [0, 0, 1])
        >>> circle = Circle.from_plane_and_radius(plane, 5)

        """
        frame = Frame.from_plane(plane)
        return cls(frame=frame, radius=radius)

    @classmethod
    def from_three_points(cls, a, b, c):
        """Construct a circle from three points.

        Parameters
        ----------
        a : :class:`~compas.geometry.Point`
            The first point.
        b : :class:`~compas.geometry.Point`
            The second point.
        c : :class:`~compas.geometry.Point`
            The third point.

        Returns
        -------
        :class:`~compas.geometry.Circle`
            The constructed circle.

        """
        from compas.geometry import Plane
        from compas.geometry import circle_from_points

        (point, normal), radius = circle_from_points(a, b, c)
        plane = Plane(point, normal)
        return cls.from_plane_and_radius(plane, radius)

    @classmethod
    def from_points(cls, points):
        """Construct a circle from a list of at least three points.

        Parameters
        ----------
        points : list of :class:`~compas.geometry.Point`
            A list of three points defining the circle.

        Returns
        -------
        :class:`~compas.geometry.Circle`
            The constructed circle.

        Raises
        ------
        ValueError
            If the list of points contains less than three points.

        Notes
        -----
        If more than three points are provided,
        the constructed cicrle is the one that best fits the points in the least squares sense.

        """
        if len(points) < 3:
            raise ValueError("At least three points are required to define a circle.")

        if len(points) == 3:
            return cls.from_three_points(*points)

        # not sure if this makes sense
        # but it will only throw an error if the bestfit is actually needed
        from compas.geometry import bestfit_circle_numpy

        point, normal, radius = bestfit_circle_numpy(points)
        plane = Plane(point, normal)
        return cls.from_plane_and_radius(plane, radius)

    # =============================================================================
    # Methods
    # =============================================================================

    def point_at(self, t):
        """Construct a point on the circle at a specific parameter.

        Parameters
        ----------
        t : float
            The parameter of the point.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The point on the circle at the specified parameter.

        """
        t = t * PI2
        x = self.radius * cos(t)
        y = self.radius * sin(t)
        return self.frame.point + self.frame.xaxis * x + self.frame.yaxis * y

    def normal_at(self, t):
        """Construct a normal on the circle at a specific parameter.

        Parameters
        ----------
        t : float
            The parameter of the normal.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The normal on the circle at the specified parameter.

        """
        normal = self.point_at(t) - self.center
        normal.unitize()
        return normal

    def tangent_at(self, t):
        """Construct a tangent on the circle at a specific parameter.

        Parameters
        ----------
        t : float
            The parameter of the tangent.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The tangent on the circle at the specified parameter.

        """
        t = t * PI2
        x = -self.radius * sin(t)
        y = +self.radius * cos(t)
        return self.frame.xaxis * x + self.frame.yaxis * y

    def binormal_at(self, t):
        """Construct a binormal on the circle at a specific parameter.

        Parameters
        ----------
        t : float
            The parameter of the binormal.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The binormal on the circle at the specified parameter.

        """
        return self.frame.zaxis

    def closest_point(self, point):
        """Compute the closest point on the circle to a given point.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            A point.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The closest point on the circle.

        """
        from compas.geometry import Vector

        projected = self.plane.closest_point(point)
        vector = Vector.from_start_end(self.center, projected)
        vector.unitize()
        vector *= self.radius
        return self.center + vector

    def contains_point(self, point, tol=1e-6, dmax=1e-6):
        """Verify that the circle contains a given point.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The point.
        tol : float, optional
            The tolerance for the verification.
        dmax : float, optional
            The maximum allowed distance between the plane of the circle and the point.

        Returns
        -------
        bool
            True if the point is on the circle.
            False otherwise.

        Notes
        -----
        By default, the verification will fail if the point is not exactly in the plane of the circle.
        To allow for a certain tolerance, use the ``dmax`` parameter.
        Like with apparent intersections, using a ``dmax`` higher than zero, allows for "apparent containment" checks

        """
        point = self.frame.to_local_coordinates(point)
        x, y, z = point.x, point.y, point.z  # type: ignore
        if abs(z) > dmax:
            return False
        return x**2 + y**2 <= (self.radius + tol) ** 2


# src/compas/geometry/curves/conic.py

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

from compas.geometry import Transformation
from .curve import Curve


class Conic(Curve):
    """Base class for curves that are conic sections."""

    # overwriting the __new__ method is necessary
    # to avoid triggering the plugin mechanism of the base curve class
    def __new__(cls, *args, **kwargs):
        curve = object.__new__(cls)
        curve.__init__(*args, **kwargs)
        return curve

    @property
    def eccentricity(self):
        raise NotImplementedError

    def transform(self, T):
        """
        Transform the curve.

        Parameters
        ----------
        T : :class:`compas.geometry.Transformation` | list[list[float]]
            The transformation.

        Returns
        -------
        None

        """
        self.frame.transform(T)
        self._transformation = Transformation.from_frame(self.frame)


# src/compas/geometry/curves/curve.py

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

from compas.plugins import pluggable
from compas.geometry import Geometry
from compas.geometry import Transformation
from compas.geometry import Point
from compas.geometry import Plane
from compas.geometry import Frame
from compas.utilities import linspace


@pluggable(category="factories")
def new_curve(cls, *args, **kwargs):
    curve = object.__new__(cls)
    curve.__init__(*args, **kwargs)
    return curve


class Curve(Geometry):
    """Class representing a general parametric curve.

    Parameters
    ----------
    name : str, optional
        The name of the curve.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The frame of the curve.
    point : :class:`~compas.geometry.Point`
        The origin of the curve.
        If not explicitly defined, this defaults to the origin of the frame (:attr:`frame`).
    transformation : :class:`~compas.geometry.Transformation`, read-only
        The transformation from the local coordinate system of the curve (:attr:`frame`) to the world coordinate system.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the curve.
    dimension : int, read-only
        The spatial dimension of the curve.
        In most cases this will be 3.
        For curves embedded on a surface, this is 2.
    domain : tuple[float, float]
        The domain of the parameter space of the curve.
    is_closed : bool, read-only
        True if the curve is closed.
    is_periodic : bool, read-only
        True if the curve is periodic.

    See Also
    --------
    :class:`compas.geometry.Arc`, :class:`compas.geometry.Circle`,
    :class:`compas.geometry.Ellipse`, :class:`compas.geometry.Line`,
    :class:`compas.geometry.NurbsCurve`, :class:`compas.geometry.Polyline`

    Notes
    -----
    The curve is a "pluggable". This means that it does not provide an actual implementation
    of a parametric curve, but rather serves as an interface for different backends.
    If a backend is available, it will be used to construct the curve and provide its functionality.
    This backend is referred to as the "plugin" implementation of the curve.

    To activate the plugin mechanism, the backend should provide an implementation of the :func:`new_curve` function,
    and of any other function that can be implemented through the functionality available in the backend.

    """

    def __new__(cls, *args, **kwargs):
        return new_curve(cls, *args, **kwargs)

    def __init__(self, frame=None, domain=None, name=None):
        super(Curve, self).__init__(name=name)
        self._frame = None
        self._transformation = None
        self._domain = None
        self._point = None
        if frame:
            self.frame = frame
        self.domain = domain

    def __eq__(self, other):
        raise NotImplementedError

    def __str__(self):
        return "<Curve with parameter domain {}>".format(self.domain)

    # ==============================================================================
    # Data
    # ==============================================================================

    @property
    def data(self):
        raise NotImplementedError

    @data.setter
    def data(self, data):
        raise NotImplementedError

    @classmethod
    def from_data(cls, data):
        """Construct a curve from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Curve`
            The constructed curve.

        """
        return cls(**data)

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def frame(self):
        if not self._frame:
            self._frame = Frame.worldXY()
        return self._frame

    @frame.setter
    def frame(self, frame):
        if not frame:
            self._frame = None
        else:
            self._frame = Frame(frame[0], frame[1], frame[2])
        self._transformation = None

    @property
    def transformation(self):
        if not self._transformation:
            self._transformation = Transformation.from_frame(self.frame)
        return self._transformation

    @property
    def point(self):
        if not self._point:
            return self.frame.point
        return self._point

    @point.setter
    def point(self, point):
        self._point = Point(*point)

    @property
    def xaxis(self):
        return self.frame.xaxis

    @property
    def yaxis(self):
        return self.frame.yaxis

    @property
    def zaxis(self):
        return self.frame.zaxis

    @property
    def plane(self):
        return Plane(self.frame.point, self.frame.zaxis)

    @property
    def dimension(self):
        return 3

    @property
    def domain(self):
        if not self._domain:
            self._domain = (0.0, 1.0)
        return self._domain

    @domain.setter
    def domain(self, domain):
        if not domain:
            self._domain = None
        else:
            u, v = domain
            self._domain = u, v

    @property
    def is_closed(self):
        return self.point_at(self.domain[0]) == self.point_at(self.domain[1])

    @property
    def is_periodic(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_step(cls, filepath):
        """Load a curve from a STP file.

        Parameters
        ----------
        filepath : str
            The path to the file.

        Returns
        -------
        :class:`~compas.geometry.Curve`

        """
        raise NotImplementedError

    @classmethod
    def from_obj(cls, filepath):
        """Load a curve from an OBJ file.

        Parameters
        ----------
        filepath : str
            The path to the file.

        Returns
        -------
        :class:`~compas.geometry.Curve`

        """
        raise NotImplementedError

    # ==============================================================================
    # Conversions
    # ==============================================================================

    def to_step(self, filepath, schema="AP203"):
        """Write the curve geometry to a STP file.

        Parameters
        ----------
        filepath : str
            The path of the output file.
        schema : str, optional
            The STEP schema to use. Default is ``"AP203"``.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def to_obj(self, filepath):
        """Write the curve geometry to an OBJ file.

        Parameters
        ----------
        filepath : str
            The path of the output file.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def to_polyline(self, resolution=10):
        """Convert the curve to a polyline.

        Parameters
        ----------
        resolution : int, optional
            The number of segments in the polyline.
            Default is ``10``.

        Returns
        -------
        :class:`~compas.geometry.Polyline`

        """
        from compas.geometry import Polyline

        points = [self.point_at(t) for t in self.space(resolution)]
        return Polyline(points)

    # ==============================================================================
    # Transformations
    # ==============================================================================

    def transform(self, T):
        """Transform the local coordinate system of the curve.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation` | list[list[float]]
            The transformation.

        Returns
        -------
        None
            The curve is modified in-place.

        Notes
        -----
        The transformation matrix is applied to the local coordinate system of the curve.
        Transformations are limited to (combinations of) translations and rotations.
        All other components of the transformation matrix are ignored.

        """
        T[0, 0] = 1
        T[1, 1] = 1
        T[2, 2] = 1
        T[3, 3] = 1

        T[0, 3] = 0
        T[1, 3] = 0
        T[2, 3] = 0

        T[3, 0] = 0
        T[3, 1] = 0
        T[3, 2] = 0

        self.frame.transform(T)

    # ==============================================================================
    # Methods
    # ==============================================================================

    def normalize_parameter(self, t):
        """Normalize a parameter to the domain of the curve.

        Parameters
        ----------
        t : float
            The parameter.

        Returns
        -------
        float
            The normalized parameter.

        """
        t = self.domain[0] + t * (self.domain[1] - self.domain[0])
        return t

    def reverse(self):
        """Reverse the parametrisation of the curve.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def reversed(self):
        """Reverse a copy of the curve.

        Returns
        -------
        :class:`~compas.geometry.Curve`

        """
        copy = self.copy()
        copy.reverse
        return copy

    def point_at(self, t):
        """Compute a point of the curve at a parameter.

        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.

        Raises
        ------
        ValueError
            If the parameter is not in the curve domain.

        """
        raise NotImplementedError

    def normal_at(self, t):
        """Compute the normal of the curve at a parameter.

        Parameters
        ----------
        t : float
            The value of the curve parameter.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The corresponding normal vector.

        Raises
        ------
        ValueError
            If the parameter is not in the curve domain.

        """
        raise NotImplementedError

    def tangent_at(self, t):
        """Compute the tangent vector of the curve at a parameter.

        Parameters
        ----------
        t : float
            The value of the curve parameter.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The corresponding tangent vector.

        Raises
        ------
        ValueError
            If the parameter is not in the curve domain.

        """
        raise NotImplementedError

    def binormal_at(self, t):
        """Compute the binormal vector of the curve at a parameter.

        Parameters
        ----------
        t : float
            The value of the curve parameter.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The corresponding binormal vector.

        Raises
        ------
        ValueError
            If the parameter is not in the curve domain.

        """
        # if not (self.domain[0] <= t <= self.domain[1]):
        #     raise ValueError("Parameter not in curve domain.")
        return self.tangent_at(t).cross(self.normal_at(t))

    def frame_at(self, t):
        """Compute the local frame of the curve at a parameter.

        Parameters
        ----------
        t : float
            The value of the curve parameter.
        world : bool, optional
            If True, return the frame in world coordinates.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The corresponding local frame.

        Raises
        ------
        ValueError
            If the parameter is not in the curve domain.

        """
        return Frame(self.point_at(t), self.tangent_at(t), self.normal_at(t))

    def curvature_at(self, t):
        """Compute the curvature vector of the curve at a parameter.

        This is a vector pointing from the point on the curve at the specified parameter,
        to the center of the oscillating circle of the curve at that location.

        Note that this vector is parallel to the normal vector of the curve at that location.

        Parameters
        ----------
        t : float
            The value of the curve parameter.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The corresponding curvature vector.

        Raises
        ------
        ValueError
            If the parameter is not in the curve domain.

        """
        raise NotImplementedError

    # ==============================================================================
    # Methods continued
    # ==============================================================================

    def space(self, n=10):
        """Compute evenly spaced parameters over the curve domain.

        Parameters
        ----------
        n : int, optional
            The number of values in the parameter space.

        Returns
        -------
        list[float]

        """
        start, end = self.domain
        return linspace(start, end, n)

    def locus(self, resolution=100):
        """Compute the locus of points on the curve.

        Parameters
        ----------
        resolution : int
            The number of intervals at which a point on the
            curve should be computed.

        Returns
        -------
        list[:class:`~compas.geometry.Point`]
            Points along the curve.

        """
        return [self.point_at(t) for t in self.space(resolution)]

    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 test point.
        return_parameter : bool, optional
            If True, the parameter corresponding to the closest point should be returned in addition to the point.

        Returns
        -------
        :class:`~compas.geometry.Point` | tuple[:class:`~compas.geometry.Point`, float]
            If `return_parameter` is False (default), only the closest point is returned.
            If `return_parameter` is True, the closest point and the corresponding parameter are returned.

        """
        raise NotImplementedError

    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] | tuple[list[float], list[:class:`~compas.geometry.Point`]]
            If `return_points` is False, the parameters of the discretisation.
            If `return_points` is True, a list of points in addition to the parameters of the discretisation.

        """
        raise NotImplementedError

    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] | tuple[list[float], list[:class:`~compas.geometry.Point`]]
            If `return_points` is False, the parameters of the discretisation.
            If `return_points` is True, a list of points in addition to the parameters of the discretisation.

        """
        raise NotImplementedError

    def aabb(self):
        """Compute the axis aligned bounding box of the curve.

        Returns
        -------
        :class:`~compas.geometry.Box`

        """
        raise NotImplementedError

    def length(self, precision=1e-8):
        """Compute the length of the curve.

        Parameters
        ----------
        precision : float, optional
            Required precision of the calculated length.

        """
        raise NotImplementedError

    def fair(self, tol=1e-3):
        raise NotImplementedError

    def offset(self):
        raise NotImplementedError

    def smooth(self):
        raise NotImplementedError

    def split(self):
        raise NotImplementedError

    def trim(self):
        raise NotImplementedError


# src/compas/geometry/curves/ellipse.py

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

from math import sqrt, pi, cos, sin

from compas.geometry import Frame
from .curve import Curve
from .line import Line

PI2 = 2 * pi


class Ellipse(Curve):
    """An ellipse is a curve defined by a coordinate system and a major and minor axis.

    The centre of the ellipse is at the origin of the coordinate system.
    The major axis is parallel to the local x-axis.
    The minor axis is parallel to the local y-axis.
    The parameter domain of an ellipse is ``[0, 2*pi]``.
    Moving along the ellipse in the parameter direction corresponds to moving counter-clockwise around the origin of the local coordinate system.

    Transformations of the ellipse are performed by transforming the coordinate system.
    They are limited to (combinations of) translations and rotations.
    All other components of transformations are ignored.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`
        The local coordinate system of the ellipse.
        The default value is ``None``, in which case the ellipse is constructed in the XY plane of the world coordinate system.
    major : float
        The major of the ellipse.
    minor : float
        The minor of the ellipse.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The coordinate frame of the ellipse.
    major : float
        The major of the ellipse.
    minor : float
        The minor of the ellipse.
    center : :class:`~compas.geometry.Point`
        The center of the ellipse.
    normal : :class:`~compas.geometry.Vector`, read-only
        The normal of the ellipse.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the ellipse.
    area : float, read-only
        The area of the ellipse.
    circumference : float, read-only
        The length of the circumference of the ellipse.
    semifocal : float, read-only
        The semi-focal distance of the ellipse.
        This is the distance from the center of the ellipse to the focus points.
    focal : float, read-only
        The distance between the two focus points.
    eccentricity : float, read-only
        The eccentricity of the ellipse.
        This is the ratio between the semifocal length to the length of the semi-major axis.
    focus1 : :class:`~compas.geometry.Point`, read-only
        The first focus point of the ellipse.
    focus2 : :class:`~compas.geometry.Point`, read-only
        The second focus point of the ellipse.
    directix1 : :class:`~compas.geometry.Line`, read-only
        The first directix of the ellipse.
        The directix is perpendicular to the major axis
        and passes through a point at a distance ``major **2 / semifocal`` along the positive xaxis from the center of the ellipse.
    directix2 : :class:`~compas.geometry.Line`, read-only
        The second directix of the ellipse.
        The directix is perpendicular to the major axis
        and passes through a point at a distance ``major **2 / semifocal`` along the negative xaxis from the center of the ellipse.
    domain : tuple(float, float), read-only
        The parameter domain of the ellipse.
    start : :class:`~compas.geometry.Point`, read-only
        The start point of the ellipse.
    end : :class:`~compas.geometry.Point`, read-only
        The end point of the ellipse.
    transformation : :class:`Transformation`, read-only
        The transformation from the local coordinate system of the ellipse (:attr:`frame`) to the world coordinate system.
    is_closed : bool, read-only
        ``True`` if the ellipse is closed.
    is_periodic : bool, read-only
        ``True`` if the ellipse is periodic.
    is_circle : bool, read-only
        ``True`` if the ellipse is a circle.

    Examples
    --------
    >>> from compas.geometry import Frame, Ellipse
    >>> ellipse = Ellipse(frame=Frame.worldXY(), major=2, minor=1)
    >>> ellipse = Ellipse(major=2, minor=1)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "major": {"type": "number", "minimum": 0},
            "minor": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "major", "minor"],
    }

    def __init__(self, frame=None, major=1.0, minor=1.0, **kwargs):
        super(Ellipse, self).__init__(frame=frame, **kwargs)
        self._major = None
        self._minor = None
        self.major = major
        self.minor = minor

    def __repr__(self):
        return "Ellipse({0!r}, {1!r}, {2!r})".format(self.frame, self.major, self.minor)

    def __len__(self):
        return 3

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.major
        elif key == 2:
            return self.minor
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.major = value
        elif key == 2:
            self.minor = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.major, self.minor])

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "major": self.major, "minor": self.minor}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.major = data["major"]
        self.minor = data["minor"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def center(self):
        return self.frame.point

    @center.setter
    def center(self, point):
        self.frame.point = point

    @property
    def major(self):
        if not self._major:
            raise ValueError("Length of major axis is not set.")
        return self._major

    @major.setter
    def major(self, major):
        if major < 0:
            raise ValueError("Major axis length cannot be negative.")
        self._major = float(major)

    @property
    def minor(self):
        if not self._minor:
            raise ValueError("Length of minor axis is not set.")
        return self._minor

    @minor.setter
    def minor(self, minor):
        if minor < 0:
            raise ValueError("Minor axis length cannot be negative.")
        self._minor = float(minor)

    @property
    def semifocal(self):
        return sqrt(self.major**2 - self.minor**2)

    @property
    def focal(self):
        return 2 * self.semifocal

    @property
    def eccentricity(self):
        return self.semifocal / self.major

    @property
    def focus1(self):
        return self.center + self.xaxis * +self.semifocal

    @property
    def focus2(self):
        return self.center + self.xaxis * -self.semifocal

    @property
    def vertex1(self):
        return self.center + self.xaxis * self.major

    @property
    def vertex2(self):
        return self.center + self.xaxis * -self.major

    @property
    def directix1(self):
        d1 = self.major**2 / self.semifocal
        p1 = self.center + self.xaxis * +d1
        return Line.from_point_and_vector(p1, self.yaxis)

    @property
    def directix2(self):
        d2 = self.major**2 / self.semifocal
        p2 = self.center + self.xaxis * -d2
        return Line.from_point_and_vector(p2, self.yaxis)

    @property
    def area(self):
        return pi * self.major * self.minor

    @property
    def circumference(self):
        raise NotImplementedError

    @property
    def is_circle(self):
        return self.major == self.minor

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_point_and_major_and_minor(cls, point, major, minor):
        """Construct a ellipse from a point and major and minor axis lengths.

        Parameters
        ----------
        point : :class:`compas.geometry.Point`
            The center point of the ellipse.
        major : float
            The major axis length.
        minor : float
            The minor axis length.

        Returns
        -------
        :class:`Ellipse`
            The constructed ellipse.

        """
        frame = Frame(point, [1, 0, 0], [0, 1, 0])
        return cls(frame, major, minor)

    # ==========================================================================
    # Methods
    # ==========================================================================

    def point_at(self, t):
        """Compute the point at a specific parameter.

        Parameters
        ----------
        t : float
            The parameter value.

        Returns
        -------
        :class:`compas.geometry.Point`
            The point at the parameter.

        """
        t = t * PI2
        x = self.major * cos(t)
        y = self.minor * sin(t)
        return self.center + self.xaxis * x + self.yaxis * y

    def tangent_at(self, t):
        """Compute the tangent at a specific parameter.

        Parameters
        ----------
        t : float
            The parameter value.

        Returns
        -------
        :class:`compas.geometry.Vector`
            The tangent vector at the parameter.

        """
        yaxis = self.normal_at(t)
        zaxis = self.zaxis
        return yaxis.cross(zaxis).unitized()

    def normal_at(self, t):
        """Compute the normal at a specific parameter.

        Parameters
        ----------
        t : float
            The parameter value.

        Returns
        -------
        :class:`compas.geometry.Vector`
            The normal vector at the parameter.

        """
        point = self.point_at(t)
        f1 = self.focus1  # in WCS
        f2 = self.focus2  # in WCS
        normal = (f1 - point).unitized() + (f2 - point).unitized()
        return normal.unitized()


# src/compas/geometry/curves/hyperbola.py

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

from math import cos, sin, sqrt
from math import pi

from compas.geometry import Frame
from .conic import Conic

PI2 = 2 * pi


class Hyperbola(Conic):
    r"""
    A hyperbola is defined by a coordinate frame and a major and minor axis.

    It is implemented using the equation

    .. math::

        \frac{x^2}{a^2} - \frac{y^2}{b^2} = 1

    and with parametric form

    .. math::

        x(t) &= a \times \sec(t) \\
        y(t) &= b \times \tan(t)

    This means that the center of the hyperbola is at the center of the coordinate frame,
    the vertices of the left and right branches are at (0, -a) and (0, +a) respectively,
    the linear eccentricity is math::`\sqrt{a^2 + b^2}`,
    and the eccentricity math::`\fraq{\sqrt{a^2 + b^2}}{a}`.

    Parameters
    ----------
    frame : :class:`compas.geometry.Frame`
        The coordinate frame of the hyperbola.
    major : float
        The major of the hyperbola.
    minor : float
        The minor of the hyperbola.

    Attributes
    ----------
    frame : :class:`compas.geometry.Frame`
        The coordinate frame of the hyperbola.
    major : float
        The major radius of the hyperbola.
    minor : float
        The minor radius of the hyperbola.
    center : :class:`compas.geometry.Point`
        The center of the hyperbola.
    normal : :class:`compas.geometry.Vector`, read-only
        The normal of the hyperbola.
    plane : :class:`compas.geometry.Plane`, read-only
        The plane of the hyperbola.
    semifocal : float, read-only
        The distance between the center and the focus points.
    focal : float, read-only
        The distance between the two focus points.
    eccentricity : float, read-only
        This is the ratio between the semifocal length to the length of the semi-major axis.
        The eccentricity of a hyperbola is a number higher than 1.
    vertex1 : :class:`compas.geometry.Point`, read-only
        The first vertex of the hyperbola is on the positive x axis.
    vertex2 : :class:`compas.geometry.Point`, read-only
        The second vertex of the hyperbola is on the negative x axis.
    focus1 : :class:`compas.geometry.Point`, read-only
        The first focus of the hyperbola is on the positive x axis.
    focus2 : :class:`compas.geometry.Point`, read-only
        The second focus of the hyperbola is on the negative x axis.
    asymptote1 : :class:`compas.geometry.Line`, read-only
        The first asymptote of the hyperbola.
    asymptote2 : :class:`compas.geometry.Line`, read-only
        The second asymptote of the hyperbola.
    domain : tuple[float, float], read-only
        The parameter domain: 0, 2pi
    is_closed : bool, read-only
        A hyperbola is not closed (False).
    is_periodic : bool, read-only
        A hyperbola is not periodic (False).

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "major": {"type": "number", "minimum": 0},
            "minor": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "major", "minor"],
    }

    def __init__(self, frame=None, major=1.0, minor=1.0, **kwargs):
        super(Hyperbola, self).__init__(frame=frame, **kwargs)
        self._major = None
        self._minor = None
        self.major = major
        self.minor = minor

    def __repr__(self):
        return "Hyperbola({0!r}, {1!r}, {2!r})".format(self.frame, self.major, self.minor)

    def __len__(self):
        return 3

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.major
        elif key == 2:
            return self.minor
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.major = value
        elif key == 2:
            self.minor = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.major, self.minor])

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "major": self.major, "minor": self.minor}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.major = data["major"]
        self.minor = data["minor"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def center(self):
        return self.frame.point

    @center.setter
    def center(self, point):
        self.frame.point = point

    @property
    def major(self):
        if not self._major:
            raise ValueError("Length of major axis is not set.")
        return self._major

    @major.setter
    def major(self, major):
        if major < 0:
            raise ValueError("Major axis length cannot be negative.")
        self._major = float(major)

    @property
    def minor(self):
        if not self._minor:
            raise ValueError("Length of minor axis is not set.")
        return self._minor

    @minor.setter
    def minor(self, minor):
        if minor < 0:
            raise ValueError("Minor axis length cannot be negative.")
        self._minor = float(minor)

    @property
    def semifocal(self):
        return sqrt(self.major**2 + self.minor**2)

    @property
    def focal(self):
        return 2 * self.semifocal

    @property
    def eccentricity(self):
        return self.semifocal / self.major

    @property
    def focus1(self):
        return self.point + self.xaxis * self.semifocal

    @property
    def focus2(self):
        return self.point + self.xaxis * -self.semifocal

    @property
    def vertex1(self):
        return self.point + self.xaxis * self.major

    @property
    def vertex2(self):
        return self.point + self.xaxis * -self.major

    @property
    def asymptote1(self):
        pass

    @property
    def asymptote2(self):
        pass

    # ==========================================================================
    # Constructors
    # ==========================================================================

    # ==========================================================================
    # Methods
    # ==========================================================================

    def point_at(self, t):
        """
        Point at the parameter.

        Parameters
        ----------
        t : float

        Returns
        -------
        :class:`compas_future.geometry.Point`

        """
        t = t * PI2
        sec_t = 1 / cos(t)
        x = self.major * sec_t
        y = self.minor * sin(t) * sec_t
        return self.center + self.xaxis * x + self.yaxis * y

    def tangent_at(self, t):
        """
        Tangent vector at the parameter.

        Parameters
        ----------
        t : float
            The line parameter.

        Returns
        -------
        :class:`compas_future.geometry.Vector`

        """
        raise NotImplementedError

    def normal_at(self, t):
        """
        Normal at a specific normalized parameter.

        Parameters
        ----------
        t : float
            The line parameter.

        Returns
        -------
        :class:`compas_future.geometry.Vector`

        """
        raise NotImplementedError

    def frame_at(self, t):
        """
        Frame at the parameter.

        Parameters
        ----------
        t : float
            The line parameter.

        Returns
        -------
        :class:`compas_future.geometry.Frame`

        """
        point = self.point_at(t)
        xaxis = self.tangent_at(t)
        yaxis = self.zaxis.cross(xaxis)
        return Frame(point, xaxis, yaxis)


# src/compas/geometry/curves/line.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 Frame
from .curve import Curve


class Line(Curve):
    """A line is a curve defined by two points.

    The first point is the start point of the line.
    The second point is the end point of the line.
    The vector between the two points defines the direction of the line.
    The length of the vector is the length of the line.
    The direction vector is the unit vector of the vector between start and end.
    The parameterisation of the line is such that the start point corresponds to ``t = 0`` and the end point to ``t = 1``.

    The coordinate system of a line is always the world coordinate system (WCS).
    Transformation of a line is performed by transforming the start and end point.

    Parameters
    ----------
    p1 : [float, float, float] | :class:`~compas.geometry.Point`
        The first point.
    p2 : [float, float, float] | :class:`~compas.geometry.Point`
        The second point.

    Attributes
    ----------
    start : :class:`~compas.geometry.Point`
        The start point of the line.
    end : :class:`~compas.geometry.Point`
        The end point of the line.
    vector : :class:`~compas.geometry.Vector`, read-only
        A vector pointing from start to end.
    length : float, read-only
        The length of the vector from start to end.
    direction : :class:`~compas.geometry.Vector`, read-only
        A unit vector parallel to the line vector.
    midpoint : :class:`~compas.geometry.Point`, read-only
        The midpoint between start and end.
    domain : tuple of float, read-only
        The parameterisation domain of the line.
        For a line, this is always ``[0, 1]``.
    frame : :class:`~compas.geometry.Frame`, read-only
        The frame of the line.
        This is alsways the world XY frame.
    transformation : :class:`~compas.geometry.Transformation`, read-only
        This is always the identity transformation.

    Examples
    --------
    >>> line = Line([0, 0, 0], [1, 1, 1])
    >>> line
    Line(Point(0.000, 0.000, 0.000), Point(1.000, 1.000, 1.000))
    >>> line.start
    Point(0.000, 0.000, 0.000)
    >>> line.midpoint
    Point(0.500, 0.500, 0.500)
    >>> line.length == math.sqrt(line.dx **2 + line.dy **2 + line.dz **2)
    True
    >>> line.direction
    Vector(0.577, 0.577, 0.577)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "start": Point.JSONSCHEMA,
            "end": Point.JSONSCHEMA,
        },
        "required": ["start", "end"],
    }

    # overwriting the __new__ method is necessary
    # to avoid triggering the plugin mechanism of the base curve class
    def __new__(cls, *args, **kwargs):
        curve = object.__new__(cls)
        curve.__init__(*args, **kwargs)
        return curve

    def __init__(self, start, end, **kwargs):
        super(Line, self).__init__(**kwargs)
        self._point = None
        self._vector = None
        self.start = start
        self.end = end

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        return {"start": self.start, "end": self.end}

    @data.setter
    def data(self, data):
        self.start = data["start"]
        self.end = data["end"]

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def frame(self):
        return Frame.worldXY()

    @frame.setter
    def frame(self, frame):
        raise Exception("Setting the coordinate frame of a line is not supported.")

    @property
    def point(self):
        if not self._point:
            raise ValueError("The line has no base point.")
        return self._point

    @point.setter
    def point(self, point):
        self._point = Point(*point)

    @property
    def vector(self):
        if not self._vector:
            raise ValueError("The line has no direction vector.")
        return self._vector

    @vector.setter
    def vector(self, vector):
        self._vector = Vector(*vector)

    @property
    def length(self):
        return self.vector.length

    @property
    def direction(self):
        return self.vector.unitized()

    @property
    def start(self):
        return self.point

    @start.setter
    def start(self, point):
        self.point = point

    @property
    def end(self):
        return self.start + self.vector

    @end.setter
    def end(self, point):
        self._vector = Vector.from_start_end(self.start, point)

    @property
    def midpoint(self):
        return self.point_at(0.5)

    # ==========================================================================
    # Customization
    # ==========================================================================

    def __repr__(self):
        return "Line({0!r}, {1!r})".format(self.start, self.end)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.start
        if key == 1:
            return self.end
        raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.start = value
            return
        if key == 1:
            self.end = value
            return
        raise KeyError

    def __iter__(self):
        return iter([self.start, self.end])

    def __eq__(self, other):
        try:
            other_start = other[0]
            other_end = other[1]
        except Exception:
            return False
        return self.start == other_start and self.end == other_end

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_point_and_vector(cls, point, vector):
        """Construct a line from a point and a vector.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The start point of the line.
        vector : :class:`~compas.geometry.Vector`
            The vector of the line.

        Returns
        -------
        :class:`Line`
            The constructed line.

        Examples
        --------
        >>> from compas.geometry import Point, Vector
        >>> line = Line.from_point_and_vector(Point(0, 0, 0), Vector(1, 1, 1))
        >>> line
        Line(Point(0.000, 0.000, 0.000), Point(1.000, 1.000, 1.000))

        """
        return cls(point, point + vector)

    @classmethod
    def from_point_direction_and_length(cls, point, direction, length):
        """Construct a line from a point, a direction and a length.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The start point of the line.
        direction : :class:`~compas.geometry.Vector`
            The direction of the line.
        length : float
            The length of the line.

        Returns
        -------
        :class:`Line`
            The constructed line.

        Examples
        --------
        >>> from compas.geometry import Point, Vector
        >>> line = Line.from_point_direction_and_length(Point(0, 0, 0), Vector(1, 1, 1), 1)
        >>> line
        Line(Point(0.000, 0.000, 0.000), Point(0.577, 0.577, 0.577))

        """
        direction = Vector(*direction)
        direction.unitize()
        return cls(point, point + direction * length)

    # ==========================================================================
    # Transformations
    # ==========================================================================

    def transform(self, T):
        """Transform this line.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation`
            The transformation.

        Returns
        -------
        None

        Examples
        --------
        >>> from math import radians
        >>> from compas.geometry import Rotation
        >>> line = Line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])
        >>> R = Rotation.from_axis_and_angle([0.0, 0.0, 1.0], radians(90))
        >>> line.transform(R)
        >>> line.end
        Point(0.000, 1.000, 0.000)

        """
        self.point.transform(T)
        self.vector.transform(T)

    # ==========================================================================
    # Methods
    # ==========================================================================

    def point_at(self, t):
        """Construct a point at a specific location along the line.

        Parameters
        ----------
        t : float
            The location along the line.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The point at the specified location.

        Examples
        --------
        >>> line = Line([0, 0, 0], [1, 1, 1])
        >>> line.point_at(0.5)
        Point(0.500, 0.500, 0.500)

        """
        point = self.point + self.vector * t
        return point

    def tangent_at(self, t):
        """Construct a tangent vector at a specific location along the line.

        Parameters
        ----------
        t : float
            The location along the line.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The tangent vector at the specified location.

        Examples
        --------
        >>> line = Line([0, 0, 0], [1, 1, 1])
        >>> line.tangent_at(0.5)
        Vector(0.577, 0.577, 0.577)

        """
        vector = self.direction
        return vector

    def closest_point(self, point, return_parameter=False):
        """Compute the closest point on the line to a given point.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The point.
        return_parameter : bool, optional
            Return the parameter of the closest point on the line.
            Default is ``False``.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The closest point on the line.
        float
            The parameter of the closest point on the line.
            Only if ``return_parameter`` is ``True``.

        """
        a = self.start
        vector = point - a
        direction = self.direction
        t = vector.dot(direction)
        c = a + direction * t
        if return_parameter:
            return c, t
        return c


# src/compas/geometry/curves/nurbs.py

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

from math import sqrt

from compas.plugins import pluggable
from compas.geometry import Point
from compas.geometry import Frame

from .curve import Curve


@pluggable(category="factories")
def new_nurbscurve(cls, *args, **kwargs):
    curve = object.__new__(NurbsCurve)
    curve.__init__(*args, **kwargs)
    return curve


@pluggable(category="factories")
def new_nurbscurve_from_parameters(cls, *args, **kwargs):
    raise NotImplementedError


@pluggable(category="factories")
def new_nurbscurve_from_points(cls, *args, **kwargs):
    raise NotImplementedError


@pluggable(category="factories")
def new_nurbscurve_from_interpolation(cls, *args, **kwargs):
    raise NotImplementedError


@pluggable(category="factories")
def new_nurbscurve_from_step(cls, *args, **kwargs):
    raise NotImplementedError


class NurbsCurve(Curve):
    """A NURBS curve is defined by control points, weights, knots, and a degree.

    Parameters
    ----------
    name : str, optional
        The name of the curve.

    Attributes
    ----------
    points : list[:class:`~compas.geometry.Point`], read-only
        The control points.
    weights : list[float], read-only
        The weights of the control points.
    knots : list[float], read-only
        The knots, without multiplicity.
    knotsequence : list[float], read-only
        The complete knot vector.
    multiplicity : list[int], read-only
        The multiplicities of the knots.
    continuity : int, read-only
        The degree of continuity of the curve.
    degree : int, read-only
        The degree of the curve.
    order : int, read-only
        The order of the curve (degree + 1).

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA},
            "weights": {"type": "array", "items": {"type": "number"}},
            "knots": {"type": "array", "items": {"type": "number"}},
            "multiplicities": {"type": "array", "items": {"type": "integer"}},
            "degree": {"type": "integer", "exclusiveMinimum": 0},
            "is_periodic": {"type": "boolean"},
        },
        "additionalProperties": False,
        "minProperties": 6,
    }

    def __new__(cls, *args, **kwargs):
        return new_nurbscurve(cls, *args, **kwargs)

    def __init__(self, name=None):
        super(NurbsCurve, self).__init__(name=name)

    def __eq__(self, other):
        raise NotImplementedError

    def __str__(self):
        lines = [
            "NurbsCurve",
            "----------",
            "Points: {}".format(self.points),
            "Weights: {}".format(self.weights),
            "Knots: {}".format(self.knots),
            "Mults: {}".format(self.multiplicities),
            "Degree: {}".format(self.degree),
            "Order: {}".format(self.order),
            "Domain: {}".format(self.domain),
            "Closed: {}".format(self.is_closed),
            "Periodic: {}".format(self.is_periodic),
            "Rational: {}".format(self.is_rational),
        ]
        return "\n".join(lines)

    # ==============================================================================
    # Data
    # ==============================================================================

    @property
    def dtype(self):
        """str : The type of the object in the form of a '2-level' import and a class name."""
        return "compas.geometry/NurbsCurve"

    @property
    def data(self):
        """dict : Representation of the curve as a dict containing only native Python data."""
        return {
            "points": self.points,
            "weights": self.weights,
            "knots": self.knots,
            "multiplicities": self.multiplicities,
            "degree": self.degree,
            "is_periodic": self.is_periodic,
        }

    @data.setter
    def data(self, data):
        raise NotImplementedError

    @classmethod
    def from_data(cls, data):
        """Construct a NURBS curve from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`
            The constructed curve.

        """
        points = data["points"]
        weights = data["weights"]
        knots = data["knots"]
        multiplicities = data["multiplicities"]
        degree = data["degree"]
        is_periodic = data["is_periodic"]
        return cls.from_parameters(points, weights, knots, multiplicities, degree, is_periodic)

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def points(self):
        raise NotImplementedError

    @property
    def weights(self):
        raise NotImplementedError

    @property
    def knots(self):
        raise NotImplementedError

    @property
    def knotsequence(self):
        raise NotImplementedError

    @property
    def multiplicities(self):
        raise NotImplementedError

    @property
    def continuity(self):
        raise NotImplementedError

    @property
    def degree(self):
        raise NotImplementedError

    @property
    def order(self):
        return self.degree + 1

    @property
    def is_rational(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_step(cls, filepath):
        """Load a NURBS curve from an STP file.

        Parameters
        ----------
        filepath : str
            The path to the file.

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`
        """
        return new_nurbscurve_from_step(cls, filepath)

    @classmethod
    def from_parameters(cls, points, weights, knots, multiplicities, degree, is_periodic=False):
        """Construct a NURBS curve from explicit curve parameters.

        Parameters
        ----------
        points : list[[float, float, float] | :class:`~compas.geometry.Point`]
            The control points.
        weights : list[float]
            The weights of the control points.
        knots : list[float]
            The curve knots, without multiplicity.
        multiplicities : list[int]
            Multiplicity of the knots.
        degree : int
            Degree of the curve.
        is_periodic : bool, optional
            Flag indicating that the curve is periodic.

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        return new_nurbscurve_from_parameters(cls, points, weights, knots, multiplicities, degree, is_periodic=False)

    @classmethod
    def from_points(cls, points, degree=3):
        """Construct a NURBS curve from control points.

        Parameters
        ----------
        points : list[[float, float, float] | :class:`~compas.geometry.Point`]
            The control points.
        degree : int, optional
            The degree of the curve.

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        return new_nurbscurve_from_points(cls, points, degree=degree)

    @classmethod
    def from_interpolation(cls, points, precision=1e-3):
        """Construct a NURBS curve by interpolating a set of points.

        Parameters
        ----------
        points : list[[float, float, float] | :class:`~compas.geometry.Point`]
            A list of interpolation points.
        precision : int, optional
            The desired precision of the interpolation.

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        return new_nurbscurve_from_interpolation(cls, points, precision=1e-3)

    @classmethod
    def from_arc(cls, arc):
        """Construct a NURBS curve from an arc.

        Parameters
        ----------
        arc : :class:`~compas.geometry.Arc`

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        raise NotImplementedError

    @classmethod
    def from_circle(cls, circle):
        """Construct a NURBS curve from a circle.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        frame = Frame.from_plane(circle.plane)
        w = 0.5 * sqrt(2)
        dx = frame.xaxis * circle.radius
        dy = frame.yaxis * circle.radius
        points = [
            frame.point - dy,
            frame.point - dy - dx,
            frame.point - dx,
            frame.point + dy - dx,
            frame.point + dy,
            frame.point + dy + dx,
            frame.point + dx,
            frame.point - dy + dx,
            frame.point - dy,
        ]
        knots = [0, 1 / 4, 1 / 2, 3 / 4, 1]
        mults = [3, 2, 2, 2, 3]
        weights = [1, w, 1, w, 1, w, 1, w, 1]
        return cls.from_parameters(points=points, weights=weights, knots=knots, multiplicities=mults, degree=2)

    @classmethod
    def from_ellipse(cls, ellipse):
        """Construct a NURBS curve from an ellipse.

        Parameters
        ----------
        ellipse : :class:`~compas.geometry.Ellipse`

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        frame = Frame.from_plane(ellipse.plane)
        frame = Frame.worldXY()
        w = 0.5 * sqrt(2)
        dx = frame.xaxis * ellipse.major
        dy = frame.yaxis * ellipse.minor
        points = [
            frame.point - dy,
            frame.point - dy - dx,
            frame.point - dx,
            frame.point + dy - dx,
            frame.point + dy,
            frame.point + dy + dx,
            frame.point + dx,
            frame.point - dy + dx,
            frame.point - dy,
        ]
        knots = [0, 1 / 4, 1 / 2, 3 / 4, 1]
        mults = [3, 2, 2, 2, 3]
        weights = [1, w, 1, w, 1, w, 1, w, 1]
        return cls.from_parameters(points=points, weights=weights, knots=knots, multiplicities=mults, degree=2)

    @classmethod
    def from_line(cls, line):
        """Construct a NURBS curve from a line.

        Parameters
        ----------
        line : :class:`~compas.geometry.Line`

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        return cls.from_parameters(
            points=[line.start, line.end],
            weights=[1.0, 1.0],
            knots=[0.0, 1.0],
            multiplicities=[2, 2],
            degree=1,
        )

    # ==============================================================================
    # Conversions
    # ==============================================================================

    # ==============================================================================
    # Methods
    # ==============================================================================

    def copy(self):
        """Make an independent copy of the current curve.

        Returns
        -------
        :class:`~compas.geometry.NurbsCurve`

        """
        return NurbsCurve.from_parameters(
            self.points,
            self.weights,
            self.knots,
            self.multiplicities,
            self.degree,
            self.is_periodic,
        )


# src/compas/geometry/curves/parabola.py

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

from compas.geometry import Vector
from compas.geometry import Point
from compas.geometry import Frame
from .line import Line
from .conic import Conic


class Parabola(Conic):
    """
    A parabola is defined by a plane and a major and minor axis.
    The origin of the coordinate frame is the center of the parabola.

    The parabola in this implementation is based on the equation ``y = a * x**2``.
    Therefore it will have the y axis of the coordinate frame as its axis of symmetry.

    Parameters
    ----------
    frame : :class:`compas.geometry.Frame`
        The coordinate frame of the parabola.
    major : float
        The major of the parabola.

    Attributes
    ----------
    frame : :class:`compas.geometry.Frame`
        The coordinate frame of the hyperbola.
    focal : float
        The distance between the two focus points.
    center : :class:`compas.geometry.Point`, read-only
        The center of the parabola.
    normal : :class:`compas.geometry.Vector`, read-only
        The normal of the parabola.
    plane : :class:`compas.geometry.Plane`, read-only
        The plane of the parabola.
    latus : :class:`compas.geometry.Point`, read-only
        The latus rectum of the parabola.
    eccentricity : float, read-only
        The eccentricity of a parabola is between 0 and 1.
    focus : :class:`compas.geometry.Point`, read-only
        The focus of the parabola.
    directix : :class:`compas.geometry.Line`, read-only
        The directix is the line perpendicular to the y axis of the parabola
        at a distance ``d = + major / eccentricity`` from the center of the parabola.
        The second directix intersects the positive x axis.
    domain : tuple[float, float], read-only
        The parameter domain: 0, 2pi
    is_closed : bool, read-only
        An parabola is closed (True).
    is_periodic : bool, read-only
        An parabola is periodic (True).

    """

    def __init__(self, frame=None, focal=1.0, **kwargs):
        super(Parabola, self).__init__(frame=frame, **kwargs)
        self._focal = None
        self.focal = focal

    def __repr__(self):
        return "Parabola({0!r}, {1!r})".format(self.frame, self.focal)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.focal
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.focal = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.focal])

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "focal": self.focal}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.focal = data["focal"]

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def focal(self):
        if self._focal is None:
            raise ValueError("The focal length of the parabola is not set.")
        return self._focal

    @focal.setter
    def focal(self, focal):
        self._focal = focal

    @property
    def a(self):
        return 1 / (4 * self.focal)

    @a.setter
    def a(self, a):
        self.focal = 1 / (4 * a)

    @property
    def eccentricity(self):
        return 1

    @property
    def latus(self):
        return 2 * self.focal

    @property
    def focus(self):
        return self.point + self.yaxis * self.focal

    @property
    def vertex(self):
        return self.point

    @property
    def directix(self):
        point = self.point + self.yaxis * -self.focal
        return Line(point, point + self.xaxis)

    # ==========================================================================
    # Constructors
    # ==========================================================================

    # ==========================================================================
    # Methods
    # ==========================================================================

    def point_at(self, t):
        """
        Point at the parameter.

        Parameters
        ----------
        t : float

        Returns
        -------
        :class:`compas_future.geometry.Point`

        """
        x = t
        y = self.a * x**2
        z = 0
        point = Point(x, y, z)
        # point.transform(self.transformation)
        return point

    def tangent_at(self, t):
        """
        Tangent vector at the parameter.

        Parameters
        ----------
        t : float
            The line parameter.

        Returns
        -------
        :class:`compas_future.geometry.Vector`

        """
        x0 = t
        y0 = self.a * t**2
        x = 2 * t
        y = 2 * self.a * x0 * x - y0
        tangent = Vector(x - x0, y - y0, 0)
        tangent.unitize()
        tangent.transform(self.transformation)
        return tangent

    def normal_at(self, t):
        """
        Normal at a specific normalized parameter.

        Parameters
        ----------
        t : float
            The line parameter.

        Returns
        -------
        :class:`compas_future.geometry.Vector`

        """
        x0 = t
        y0 = self.a * t**2
        x = 2 * t
        y = 2 * self.a * x0 * x - y0
        normal = Vector(y0 - y, x - x0, 0)
        normal.unitize()
        normal.transform(self.transformation)
        return normal

    def frame_at(self, t):
        """
        Frame at the parameter.

        Parameters
        ----------
        t : float
            The line parameter.

        Returns
        -------
        :class:`compas_future.geometry.Frame`

        """
        point = self.point_at(t)
        xaxis = self.tangent_at(t)
        yaxis = self.frame.zaxis.cross(xaxis)
        return Frame(point, xaxis, yaxis)


# src/compas/geometry/curves/polyline.py

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

from compas.utilities import pairwise

from compas.geometry import allclose
from compas.geometry import transform_points
from compas.geometry import is_point_on_line
from compas.geometry import Point
from compas.geometry import Line
from compas.geometry import Frame
from .curve import Curve


class Polyline(Curve):
    """A polyline is a curve defined by a sequence of points connected by line segments.

    A Polyline can be open or closed.
    It can be self-intersecting.
    It does not have an interior.

    The parameter space is defined along the consecutive direction vectors of the line segments of the polyline.
    The coordinate system of the parametrisation is the world coordinate system.
    Transformations of polylines are defined as transformations of the points defining the polyline.

    Parameters
    ----------
    points : list[[float, float, float] | :class:`~compas.geometry.Point`]
        An ordered list of points.
        Each consecutive pair of points forms a segment of the polyline.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`, read-only
        The frame of the spatial coordinates of the polyline.
        This is the world XY frame.
    points : list[:class:`~compas.geometry.Point`]
        The points of the polyline.
    lines : list[:class:`~compas.geometry.Line`], read-only
        The lines of the polyline.
    length : float, read-only
        The length of the polyline.
    start : :class:`~compas.geometry.Point`, read-only
        The start point of the polyline.
    end : :class:`~compas.geometry.Point`, read-only
        The end point of the polyline.
    is_selfintersecting : bool, read-only
        True if the polyline is self-intersecting.
    is_closed : bool, read-only
        True if the polyline is closed.

    Examples
    --------
    >>> polyline = Polyline([[0,0,0], [1,0,0], [2,0,0], [3,0,0]])
    >>> polyline.length
    3.0

    >>> type(polyline.points[0]) == Point
    True
    >>> polyline.points[0].x
    0.0

    >>> type(polyline.lines[0]) == Line
    True
    >>> polyline.lines[0].length
    1.0

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA},
        },
        "required": ["points"],
    }

    # overwriting the __new__ method is necessary
    # to avoid triggering the plugin mechanism of the base curve class
    def __new__(cls, *args, **kwargs):
        curve = object.__new__(cls)
        curve.__init__(*args, **kwargs)
        return curve

    def __init__(self, points, **kwargs):
        super(Polyline, self).__init__(**kwargs)
        self._points = []
        self._lines = []
        self.points = points

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        return {"points": self.points}

    @data.setter
    def data(self, data):
        self.points = data["points"]

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def frame(self):
        return Frame.worldXY()

    @frame.setter
    def frame(self, frame):
        pass

    @property
    def points(self):
        return self._points

    @points.setter
    def points(self, points):
        self._points = [Point(*xyz) for xyz in points]
        self._lines = None

    @property
    def lines(self):
        if self._lines is None:
            self._lines = [Line(a, b) for a, b in pairwise(self.points)]
        return self._lines

    @property
    def length(self):
        return sum([line.length for line in self.lines])

    @property
    def start(self):
        return self.points[0]

    @property
    def end(self):
        return self.points[-1]

    @property
    def is_selfintersecting(self):
        raise NotImplementedError

    @property
    def is_closed(self):
        return self.points[0] == self.points[-1]

    # ==========================================================================
    # customization
    # ==========================================================================

    def __repr__(self):
        return "Polyline([{0}])".format(", ".join(["{0!r}".format(point) for point in self.points]))

    def __len__(self):
        return len(self.points)

    def __getitem__(self, key):
        return self.points[key]

    def __setitem__(self, key, value):
        self.points[key] = Point(*value)
        self._lines = None

    def __iter__(self):
        return iter(self.points)

    def __eq__(self, other):
        if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other):
            return False
        return allclose(self, other)

    # ==========================================================================
    # Constructors
    # ==========================================================================

    # ==========================================================================
    # Transformations
    # ==========================================================================

    def transform(self, T):
        """Transform this polyline.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation` | list[list[float]]
            The transformation.

        Examples
        --------
        >>> from math import radians
        >>> from compas.geometry import Rotation
        >>> polyline = Polyline([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]])
        >>> R = Rotation.from_axis_and_angle([0.0, 0.0, 1.0], radians(90))
        >>> polyline.transform(R)

        """
        for index, point in enumerate(transform_points(self.points, T)):
            self.points[index].x = point[0]
            self.points[index].y = point[1]
            self.points[index].z = point[2]

    # ==========================================================================
    # Methods
    # ==========================================================================

    def point_at(self, t, snap=False):
        """Point on the polyline at a specific normalized parameter.

        Parameters
        ----------
        t : float
            The parameter value.
        snap : bool, optional
            If True, return the closest polyline point.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The point on the polyline.

        Examples
        --------
        >>> polyline = Polyline([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]])
        >>> polyline.point(0.75)
        Point(1.000, 0.500, 0.000)

        """
        if t < 0 or t > 1:
            return None

        points = self.points
        if t == 0:
            return points[0]
        if t == 1:
            return points[-1]

        polyline_length = self.length

        x = 0
        i = 0
        while x <= t:
            line = Line(points[i], points[i + 1])
            line_length = line.length
            dx = line_length / polyline_length
            if x + dx > t:
                if snap:
                    if t - x < x + dx - t:
                        return line.start
                    else:
                        return line.end
                return line.point_at((t - x) * polyline_length / line_length)
            x += dx
            i += 1

    def tangent_at(self, t):
        """Tangent vector at a specific normalized parameter.

        Parameters
        ----------
        t : float
            The parameter value.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The tangent vector at the specified parameter.

        Examples
        --------
        >>> polyline = Polyline([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]])
        >>> polyline.tangent(0.75)
        Vector(0.000, 1.000, 0.000)

        """
        if t < 0 or t > 1:
            return None

        points = self.points
        if t == 0:
            return points[1] - points[0]
        if t == 1:
            return points[-1] - points[-2]

        polyline_length = self.length

        x = 0
        i = 0
        while x <= t:
            line = Line(points[i], points[i + 1])
            line_length = line.length
            dx = line_length / polyline_length
            if x + dx > t:
                return line.direction
            x += dx
            i += 1

    def tangent_at_point(self, point):
        """Calculates the tangent vector of a point on a polyline

        Parameters
        ----------
        point: [float, float, float] | :class:`~compas.geometry.Point`

        Returns
        -------
        :class:`~compas.geometry.Vector`

        """
        for line in self.lines:
            if is_point_on_line(point, line):
                return line.direction
        raise Exception("{} not found!".format(point))

    def split_at_corners(self, angle_threshold):
        """Splits a polyline at corners larger than the given angle_threshold

        Parameters
        ----------
        angle_threshold : float
            In radians.

        Returns
        -------
        list[:class:`~compas.geometry.Polyline`]

        """
        corner_ids = []
        split_polylines = []
        points = self.points
        seg_ids = list(range(len(self.lines)))

        if self.is_closed:
            seg_ids.append(0)

        for seg1, seg2 in pairwise(seg_ids):
            angle = self.lines[seg1].vector.angle(self.lines[seg2].vector)
            if angle >= angle_threshold:
                corner_ids.append(seg1 + 1)

        if self.is_closed and len(corner_ids) > 0:
            if corner_ids[-1] != len(points):
                corner_ids = [corner_ids[-1]] + corner_ids
        else:
            corner_ids = [0] + corner_ids + [len(points)]

        for id1, id2 in pairwise(corner_ids):
            if id1 < id2:
                split_polylines.append(Polyline(points[id1 : id2 + 1]))
            else:
                looped_pts = [points[i] for i in range(id1, len(points))] + points[1 : id2 + 1]
                split_polylines.append(Polyline(looped_pts))

        if self.is_closed and not corner_ids:
            return [Polyline(self.points)]

        return split_polylines

    def divide(self, num_segments):
        """Divide a polyline in equal segments.

        Parameters
        ----------
        num_segments : int

        Returns
        -------
        list
            list[:class:`~compas.geometry.Point`]

        Examples
        --------
        >>> polyline = Polyline([(0, 0, 0), (1, 1, 0), (2, 3, 0), (4, 4, 0), (5, 2, 0)])
        >>> divided_polylines = polyline.divide(3)
        >>> divided_polyline
        [Point(0.000, 0.000, 0.000), Point(1.578, 2.157, 0.000), Point(3.578, 3.789, 0.000), Point(5.000, 2.000, 0.000)]

        """
        segment_length = self.length / num_segments
        return self.divide_by_length(segment_length, False)

    def divide_by_length(self, length, strict=True, tol=1e-06):
        """Divide a polyline in segments of a given length.

        Parameters
        ----------
        length : float
            Length of the segments.
        strict : bool, optional
            If False, the remainder segment will be added even if it is smaller than the desired length
        tol : float, optional
            Floating point error tolerance.

        Returns
        -------
        list[:class:`~compas.geometry.Point`]

        Notes
        -----
        The points of the new polyline are constrained to the segments of the old polyline.
        However, since the old points are not part of the new set of points, the geometry of the polyline will change.

        Examples
        --------
        >>> polyline = Polyline([(0, 0, 0), (1, 1, 0), (2, 3, 0), (4, 4, 0), (5, 2, 0)])
        >>> divided_polylines = polyline.divide_by_length(3)
        >>> divided_polyline
        [Point(0.000, 0.000, 0.000), Point(1.709, 2.418, 0.000), Point(4.051, 3.898, 0.000)]

        >>> polyline = Polyline([(0, 0, 0), (1, 1, 0), (2, 3, 0), (4, 4, 0), (5, 2, 0)])
        >>> divided_polylines = polyline.divide_by_length(3, strict=False)
        >>> divided_polyline
        [Point(0.000, 0.000, 0.000), Point(1.709, 2.418, 0.000), Point(4.051, 3.898, 0.000), Point(5.000, 2.000, 0.000)]

        """
        num_pts = int(self.length / length)
        total_length = [0, 0]
        division_pts = [self.points[0]]
        new_polyline = self

        for i in range(num_pts):
            for i_ln, line in enumerate(new_polyline.lines):
                total_length.append(total_length[-1] + line.length)  # type: ignore
                if total_length[-1] > length:
                    amp = (length - total_length[-2]) / line.length
                    new_pt = line.start + line.vector.scaled(amp)
                    division_pts.append(new_pt)
                    total_length = [0, 0]
                    remaining_pts = new_polyline.points[i_ln + 2 :]
                    new_polyline = Polyline([new_pt, line.end] + remaining_pts)
                    break
                elif total_length[-1] == length:
                    total_length = [0, 0]
                    division_pts.append(line.end)

            if len(division_pts) == num_pts + 1:
                break

        if strict is False and not self.is_closed and len(division_pts) < num_pts + 1:
            division_pts.append(new_polyline.points[-1])
        elif strict is False and division_pts[-1].distance_to_point(self.points[-1]) > tol:
            division_pts.append(self.points[-1])

        return division_pts

    def split_by_length(self, length, strict=True):
        """Split a polyline in segments of a given length.

        Parameters
        ----------
        length : float
            Length of the segments.
        strict : bool, optional
            If False, the remainder segment will be added even if it is smaller than the desired length
        tol : float, optional
            Floating point error tolerance.

        Returns
        -------
        list[:class:`~compas.geometry.Polyline`]

        Examples
        --------
        >>> from compas.geometry import Polyline
        >>> polyline = Polyline([(0, 0, 0), (1, 1, 0), (2, 3, 0), (4, 4, 0), (5, 2, 0)])
        >>> split_polylines = polyline.split_polyline_by_length(3)
        >>> split_polylines
        [Polyline([Point(0.000, 0.000, 0.000), Point(1.000, 1.000, 0.000), Point(1.709, 2.418, 0.000)]),\
        Polyline([Point(1.709, 2.418, 0.000), Point(2.000, 3.000, 0.000), Point(4.000, 4.000, 0.000),\
        Point(4.051, 3.898, 0.000)])]

        >>> from compas.geometry import Polyline
        >>> polyline = Polyline([(0, 0, 0), (1, 1, 0), (2, 3, 0), (4, 4, 0), (5, 2, 0)])
        >>> split_polylines = polyline.split_polyline_by_length(3, strict=False)
        >>> split_polylines
        [Polyline([Point(0.000, 0.000, 0.000), Point(1.000, 1.000, 0.000), Point(1.709, 2.418, 0.000)]),\
        Polyline([Point(1.709, 2.418, 0.000), Point(2.000, 3.000, 0.000), Point(4.000, 4.000, 0.000),\
        Point(4.051, 3.898, 0.000)]), Polyline([Point(4.051, 3.898, 0.000), Point(5.000, 2.000, 0.000)])]

        """
        if length <= 0:
            raise ValueError("Length should be bigger than 0.")
        elif length > self.length:
            raise ValueError("Polyline length {0} is smaller than input length {1}.".format(self.length, length))
        divided_polylines = []
        polyline_copy = self.copy()
        segment = Polyline([self[0]])  # Start a new segment
        i, current_length = 0, 0
        polyline_points_num = len(polyline_copy)
        while i < polyline_points_num - 1:
            pt1, pt2 = polyline_copy.points[i : i + 2]
            line_length = pt1.distance_to_point(pt2)
            current_length += line_length
            if current_length <= length:
                segment.points.append(pt2)
                i += 1
            else:
                amp = 1 - ((current_length - length) / line_length)
                new_pt = pt1 + (pt2 - pt1).scaled(amp)
                polyline_copy.points.insert(i + 1, new_pt)
                segment.points.append(new_pt)
                divided_polylines.append(segment)
                segment = Polyline([new_pt])  # Start a new segment
                current_length = 0
                i += 1
                polyline_points_num = len(polyline_copy)
        if not strict and len(divided_polylines):
            divided_polylines.append(segment)  # Add the last segment
        return divided_polylines

    def split(self, num_segments):
        """Split a polyline in equal segments.

        Parameters
        ----------
        num_segments : int

        Returns
        -------
        list
            list[:class:`~compas.geometry.Polyline`]

        Examples
        --------
        >>> from compas.geometry import Polyline
        >>> polyline = Polyline([(0, 0, 0), (1, 1, 0), (2, 3, 0), (4, 4, 0), (5, 2, 0)])
        >>> split_polylines = polyline.split_polyline(3)
        >>> split_polylines
        [Polyline([Point(0.000, 0.000, 0.000), Point(1.000, 1.000, 0.000), Point(1.578, 2.157, 0.000)]),\
        Polyline([Point(1.578, 2.157, 0.000), Point(2.000, 3.000, 0.000), Point(3.578, 3.789, 0.000)]),\
        Polyline([Point(3.578, 3.789, 0.000), Point(4.000, 4.000, 0.000), Point(5.000, 2.000, 0.000)])]
        """
        if num_segments < 1:
            raise ValueError("Number of segments must be greater than or equal to 1.")
        elif num_segments == 1:
            return [self]
        total_length = self.length
        segment_length = total_length / num_segments
        return self.split_by_length(segment_length, False)

    def extend(self, length):
        """Extends a polyline by a given length, by modifying the first and/or last point tangentially.

        Parameters
        ----------
        length: float or tuple[float, float]
            A single length value to extend the polyline only at the end,
            or two length values to extend at both ends.

        Returns
        -------
        None

        """
        try:
            start, end = length
            self.points[0] = self.points[0] + self.lines[0].vector.unitized().scaled(-start)
        except TypeError:
            start = end = length
        self.points[-1] = self.points[-1] + self.lines[-1].vector.unitized().scaled(end)

    def extended(self, length):
        """Returns a copy of this polyline extended by a given length.

        Parameters
        ----------
        length: float or tuple[float, float]
            A single length value to extend the polyline only at the end,
            or two length values to extend at both ends.

        Returns
        -------
        :class:`~compas.geometry.Polyline`

        """
        crv = self.copy()
        crv.extend(length)
        return crv

    def shorten(self, length):
        """Shortens a polyline by a given length.

        Parameters
        ----------
        length: float or tuple[float, float]
            A single length value to shorten the polyline only at the end,
            or two length values to shorten at both ends.

        Returns
        -------
        None

        """
        try:
            start, end = length
            total_length = 0
            for line in self.lines:
                total_length += line.length
                if total_length < start:
                    del self.points[0]
                elif total_length == start:
                    del self.points[0]
                    break
                else:
                    self.points[0] = line.end + line.vector.unitized().scaled(-(total_length - start))
                    break
        except TypeError:
            start = end = length

        total_length = 0
        for i in range(len(self.lines)):
            line = self.lines[-(i + 1)]
            total_length += line.length
            if total_length < end:
                del self.points[-1]
            elif total_length == end:
                del self.points[-1]
                break
            else:
                self.points[-1] = line.start + line.vector.unitized().scaled(total_length - end)
                break

    def shortened(self, length):
        """Returns a copy of this polyline shortened by a given length.

        Parameters
        ----------
        length: float or tuple[float, float]
            A single length value to shorten the polyline only at the end,
            or two length values to shorten at both ends.

        Returns
        -------
        :class:`~compas.geometry.Polyline`

        """
        crv = self.copy()
        crv.shorten(length)
        return crv


# src/compas/geometry/hull/hull.py

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

from compas.geometry import cross_vectors
from compas.geometry import subtract_vectors
from compas.geometry import dot_vectors
from compas.geometry import cross_vectors_xy


def convex_hull(points):
    """Construct convex hull for a set of points.

    Parameters
    ----------
    points : sequence[point]
        A sequence of XYZ coordinates.

    Returns
    -------
    list[[int, int, int]]
        The triangular faces of the convex hull as lists of vertex indices
        referring to the original point coordinates.

    Notes
    -----
    This algorithm is based on [1]_. Note that is not optimized and relatively
    slow on large sets of points. For a more optimized version of this algorithm,
    see [2]_.

    References
    ----------
    .. [1] GitHubGist. *Convex Hull*.
           Available at: https://gist.github.com/anonymous/5184ba0bcab21d3dd19781efd3aae543
    .. [2] Thomas Diewald. *Convex Hull 3D - Quickhull Algorithm*.
           Available at: https://web.archive.org/web/20180106161310/http://thomasdiewald.com/blog/?p=1888

    Examples
    --------
    >>>

    """

    def _normal_face(face):
        u = subtract_vectors(points[face[1]], points[face[0]])
        v = subtract_vectors(points[face[-1]], points[face[0]])
        return cross_vectors(u, v)

    def _seen(face, p):
        normal = _normal_face(face)
        vec = subtract_vectors(points[p], points[face[0]])
        return dot_vectors(normal, vec) >= 0

    def _bdry(faces):
        bdry_fw = set([(face[i - 1], face[i]) for face in faces for i in range(len(face))])
        bdry_bk = set([(face[i], face[i - 1]) for face in faces for i in range(len(face))])
        return bdry_fw - bdry_bk

    def _add_point(hull, p):
        seen_faces = [face for face in hull if _seen(face, p)]

        if len(seen_faces) == len(hull):
            # if can see all faces, unsee ones looking "down"
            normal = _normal_face(seen_faces[0])
            seen_faces = [face for face in seen_faces if dot_vectors(_normal_face(face), normal) > 0]

        for face in seen_faces:
            hull.remove(face)

        for edge in _bdry(seen_faces):
            hull.append([edge[0], edge[1], p])

    hull = [[0, 1, 2], [0, 2, 1]]
    for i in range(3, len(points)):
        _add_point(hull, i)
    return hull


def convex_hull_xy(points, strict=False):
    """Computes the convex hull of a set of 2D points.

    Parameters
    ----------
    points : sequence[point]
        XY(Z) coordinates of the points.

    Returns
    -------
    list[[float, float, 0.0]]
        XY(Z) coordinates of vertices of the convex hull in counter-clockwise order,
        starting from the vertex with the lexicographically smallest coordinates.

    Notes
    -----
    Implements Andrew's monotone chain algorithm [1]_.
    O(n log n) complexity.

    References
    ----------
    .. [1] Wiki Books. *Algorithm Implementation/Geometry/Convex hull/Monotone chain*.
           Available at: https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain.

    Examples
    --------
    >>>

    """

    def cross(o, a, b):
        u = subtract_vectors(a, o)
        v = subtract_vectors(b, o)
        return cross_vectors_xy(u, v)[2]

    # Sort the points lexicographically (tuples are compared lexicographically).
    # Remove duplicates to detect the case we have just one unique point.
    points = sorted(set(map(tuple, points)))

    # Boring case: no points or a single point, possibly repeated multiple times.
    if len(points) <= 1:
        return points

    # Build lower hull
    lower = []
    for p in points:
        if strict:
            while len(lower) >= 2 and cross(lower[-2], lower[-1], p) < 0:
                lower.pop()
        else:
            while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
                lower.pop()
        lower.append(p)

    # Build upper hull
    upper = []
    for p in reversed(points):
        if strict:
            while len(upper) >= 2 and cross(upper[-2], upper[-1], p) < 0:
                upper.pop()
        else:
            while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
                upper.pop()
        upper.append(p)

    # Concatenation of the lower and upper hulls gives the convex hull.
    # Last point of each list is omitted because it is repeated at the beginning of the other list.
    return lower[:-1] + upper[:-1]


# src/compas/geometry/hull/hull_numpy.py

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

from numpy import asarray
from scipy.spatial import ConvexHull


def convex_hull_numpy(points):
    """Compute the convex hull of a set of points.

    Parameters
    ----------
    points : array_like[point]
        XYZ coordinates of the points.

    Returns
    -------
    ndarray[int](N, )
        Indices of the points on the hull.
    ndarray[int](M, 3)
        Faces of the hull.

    Raises
    ------
    AssertionError
        If the input data is not 3D.

    Notes
    -----
    The faces of the hull returned by this function do not necessarily have consistent
    cycle directions. To obtain a mesh with consistent cycle directions, construct
    a mesh from the returned vertices, this function should be used in combination
    with :func:`compas.topology.unify_cycles`.

    Examples
    --------
    >>>

    """
    points = asarray(points)
    n, dim = points.shape

    assert 2 < dim, "The point coordinates should be at least 3D: %i" % dim

    points = points[:, :3]
    hull = ConvexHull(points)

    return hull.vertices, hull.simplices


def convex_hull_xy_numpy(points):
    """Compute the convex hull of a set of points in the XY plane.

    Parameters
    ----------
    points : array_like[point]
        XY(Z) coordinates of the points.

    Returns
    -------
    ndarray[int](N, )
        Indices of the points on the hull.
    ndarray[int](M, 2)
        Lines of the hull.

    Raises
    ------
    AssertionError
        If the input data is not at least 2D.

    Examples
    --------
    >>>

    """
    points = asarray(points)
    n, dim = points.shape

    assert 1 < dim, "The point coordinates should be at least 2D: %i" % dim

    points = points[:, :2]
    hull = ConvexHull(points)
    return hull.vertices, hull.simplices


# src/compas/geometry/icp/icp_numpy.py

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

import numpy as np
from numpy import asarray
from numpy import argmin
from numpy.linalg import det
from scipy.spatial.distance import cdist
from scipy.linalg import svd
from scipy.linalg import norm

from compas.numerical import pca_numpy
from compas.numerical import normrow
from compas.geometry import transform_points_numpy


def bestfit_transform(A, B):
    n, m = A.shape
    Am = np.mean(A, axis=0)
    Bm = np.mean(B, axis=0)
    AA = A - Am
    BB = B - Bm
    # cross-covariance matrix
    C = np.dot(AA.T, BB)
    U, S, Vt = svd(C)
    # rigid rotation of the data frames
    R = np.dot(Vt.T, U.T)
    # check for RotoReflection
    if det(R) < 0:
        Vt[m - 1, :] *= -1
        R = np.dot(Vt.T, U.T)
    # translation that moves data set means to same location
    # this can be done differently (by applying three transformations (T1, R, T2))
    T = Bm.T - np.dot(R, Am.T)
    X = np.identity(m + 1)
    X[:m, :m] = R
    X[:m, m] = T
    return X


def icp_numpy(source, target, tol=1e-3):
    """Align two point clouds using the Iterative Closest Point (ICP) method.

    Parameters
    ----------
    source : array_like[point]
        The source data.
    target : array_like[point]
        The target data.
    tol : float, optional
        Tolerance for finding matches.

    Returns
    -------
    ndarray[float](N, 3)
        The transformed points.
    ndarray[float](4, 4)
        The bestfit transformation matrix.

    Notes
    -----
    First we align the source with the target cloud using the frames resulting
    from a PCA of each of the clouds, simply by calculating a frame-to-frame transformation.

    This initial alignment is used to establish an initial correspondence between
    the points of the two clouds.

    Then we iteratively improve the alignment by computing successive "best-fit"
    transformations using SVD of the cross-covariance matrix of the two data sets.
    During this iterative process, we continuously update the correspondence
    between the point clouds by finding the closest point in the target to each
    of the source points.

    The algorithm terminates when the alignment error is below a specified tolerance.

    Examples
    --------
    >>>

    """
    from compas.geometry import Transformation
    from compas.geometry import Frame

    A = asarray(source)
    B = asarray(target)

    origin, axes, _ = pca_numpy(A)
    A_frame = Frame(origin, axes[0], axes[1])

    origin, axes, _ = pca_numpy(B)
    B_frame = Frame(origin, axes[0], axes[1])

    X = Transformation.from_frame_to_frame(A_frame, B_frame)
    A = transform_points_numpy(A, X)

    for i in range(20):
        D = cdist(A, B, "euclidean")
        closest = argmin(D, axis=1)
        if norm(normrow(A - B[closest])) < tol:
            break
        X = bestfit_transform(A, B[closest])
        A = transform_points_numpy(A, X)

    return A, X


# src/compas/geometry/interpolation/barycentric.py

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

from compas.geometry import subtract_vectors
from compas.geometry import dot_vectors


def barycentric_coordinates(point, triangle):
    """Compute the barycentric coordinates of a point wrt to a triangle.

    Parameters
    ----------
    point: [float, float, float] | :class:`~compas.geometry.Point`
        Point location.
    triangle: [point, point, point]
        A triangle defined by 3 points.

    Returns
    -------
    [float, float, float]
        The barycentric coordinates of the point.

    """
    a, b, c = triangle
    v0 = subtract_vectors(b, a)
    v1 = subtract_vectors(c, a)
    v2 = subtract_vectors(point, a)
    d00 = dot_vectors(v0, v0)
    d01 = dot_vectors(v0, v1)
    d11 = dot_vectors(v1, v1)
    d20 = dot_vectors(v2, v0)
    d21 = dot_vectors(v2, v1)
    D = d00 * d11 - d01 * d01
    v = (d11 * d20 - d01 * d21) / D
    w = (d00 * d21 - d01 * d20) / D
    u = 1.0 - v - w
    return [u, v, w]


# src/compas/geometry/interpolation/coons.py

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

from compas.geometry import add_vectors
from compas.geometry import scale_vector
from compas.geometry import subtract_vectors
from compas.geometry import sum_vectors

from compas.utilities import normalize_values


def discrete_coons_patch(ab, bc, dc, ad):
    """Creates a coons patch from a set of four or three boundary
    polylines (ab, bc, dc, ad).

    Parameters
    ----------
    ab : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The XYZ coordinates of the vertices of the first polyline.
    bc : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The XYZ coordinates of the vertices of the second polyline.
    dc : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The XYZ coordinates of the vertices of the third polyline.
    ad : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The XYZ coordinates of the vertices of the fourth polyline.

    Returns
    -------
    list[[float, float, float]]
        The points of the coons patch.
    list[list[int]]
        List of faces, with every face a list of indices into the point list.

    Notes
    -----
    The vertices of the polylines are assumed to be in the following order::

        b -----> c
        ^        ^
        |        |
        |        |
        a -----> d

    To create a triangular patch, one of the input polylines should be None.
    (Warning! This will result in duplicate vertices.)

    For more information see [1]_ and [2]_.

    References
    ----------
    .. [1] Wikipedia. *Coons patch*.
           Available at: https://en.wikipedia.org/wiki/Coons_patch.
    .. [2] Robert Ferreol. *Patch de Coons*.
           Available at: https://www.mathcurve.com/surfaces/patchcoons/patchcoons.shtml

    Examples
    --------
    >>>

    """
    if not ab:
        ab = [ad[0]] * len(dc)
    if not bc:
        bc = [ab[-1]] * len(ad)
    if not dc:
        dc = [bc[-1]] * len(ab)
    if not ad:
        ad = [dc[0]] * len(bc)

    n = len(ab)
    m = len(bc)

    n_norm = normalize_values(range(n))
    m_norm = normalize_values(range(m))

    array = [[0] * m for i in range(n)]
    for i, ki in enumerate(n_norm):
        for j, kj in enumerate(m_norm):
            # first function: linear interpolation of first two opposite curves
            lin_interp_ab_dc = add_vectors(scale_vector(ab[i], (1 - kj)), scale_vector(dc[i], kj))
            # second function: linear interpolation of other two opposite curves
            lin_interp_bc_ad = add_vectors(scale_vector(ad[j], (1 - ki)), scale_vector(bc[j], ki))
            # third function: linear interpolation of four corners resulting a hypar
            a = scale_vector(ab[0], (1 - ki) * (1 - kj))
            b = scale_vector(bc[0], ki * (1 - kj))
            c = scale_vector(dc[-1], ki * kj)
            d = scale_vector(ad[-1], (1 - ki) * kj)
            lin_interp_a_b_c_d = sum_vectors([a, b, c, d])
            # coons patch = first + second - third functions
            array[i][j] = subtract_vectors(add_vectors(lin_interp_ab_dc, lin_interp_bc_ad), lin_interp_a_b_c_d)

    # create vertex list
    vertices = []
    for i in range(n):
        vertices += array[i]

    # create face vertex list
    faces = []
    for i in range(n - 1):
        for j in range(m - 1):
            faces.append([i * m + j, i * m + j + 1, (i + 1) * m + j + 1, (i + 1) * m + j])
    return vertices, faces


# src/compas/geometry/interpolation/tweening.py

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

from compas.geometry import add_vectors
from compas.geometry import subtract_vectors
from compas.geometry import scale_vector
from compas.geometry import distance_point_point


def tween_points(points1, points2, num):
    """Compute the interpolated points between two sets of points.

    Parameters
    ----------
    points1 : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The first set of points.
    points2 : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The second set of points.
    num : int
        The number of interpolated sets to return.

    Returns
    -------
    list[list[[float, float, float]]]
        Nested list of points.

    Raises
    ------
    AssertionError
        When the two point sets do not have the same length.

    Notes
    -----
    The two point sets should have the same length.

    Examples
    --------
    >>>

    """
    vectors = [subtract_vectors(p2, p1) for p1, p2 in zip(points1, points2)]
    tweens = []
    for j in range(num):
        tween = []
        for point, vector in zip(points1, vectors):
            scale = (j + 1.0) / (num + 1.0)
            tween.append(add_vectors(point, scale_vector(vector, scale)))
        tweens.append(tween)
    return tweens


def tween_points_distance(points1, points2, dist, index=None):
    """Compute an interpolated set of points between two sets of points, at
    a given distance.

    Parameters
    ----------
    points1 : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The first set of points.
    points2 : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The second set of points.
    dist : float
        The distance from the first set to the second at which to compute the interpolated set.
    index: int, optional
        The index of the point in the first set from which to calculate the distance to the second set.
        If no value is given, the first point will be used.

    Returns
    -------
    list[list[[float, float, float]]]
        List of points.

    """
    if not index:
        index = 0
    d = distance_point_point(points1[index], points2[index])
    scale = float(dist) / d
    tweens = []
    for i in range(len(points1)):
        tweens.append(
            add_vectors(
                points1[i],
                scale_vector(subtract_vectors(points2[i], points1[i]), scale),
            )
        )
    return tweens


# src/compas/geometry/intersections/__init__.py

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

from compas.plugins import pluggable


@pluggable(category="intersections")
def intersection_mesh_mesh(A, B):
    """Compute the intersection of tow meshes.

    Parameters
    ----------
    A : tuple of vertices and faces
        Mesh A.
    B : tuple of vertices and faces
        Mesh B.

    Returns
    -------
    list of arrays of points
        The intersection polylines as arrays of points.

    """
    raise NotImplementedError


@pluggable(category="intersections")
def intersection_ray_mesh(ray, mesh):
    """Compute the intersection(s) between a ray and a mesh.

    Parameters
    ----------
    ray : tuple of point and vector
        A ray represented by a point and a direction vector.
    mesh : tuple of vertices and faces
        A mesh represented by a list of vertices and a list of faces.

    Returns
    -------
    list of tuple
        Per intersection of the ray with the mesh:

        0. the index of the intersected face
        1. the u coordinate of the intersection in the barycentric coordinates of the face
        2. the u coordinate of the intersection in the barycentric coordinates of the face
        3. the distance between the ray origin and the hit

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/intersections/intersections.py

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

from math import fabs
from math import sqrt

import compas

from compas.utilities import pairwise
from compas.utilities import geometric_key

from compas.geometry import allclose
from compas.geometry import add_vectors
from compas.geometry import subtract_vectors
from compas.geometry import scale_vector
from compas.geometry import cross_vectors
from compas.geometry import dot_vectors
from compas.geometry import length_vector_xy
from compas.geometry import subtract_vectors_xy
from compas.geometry import normalize_vector
from compas.geometry import distance_point_point
from compas.geometry import is_point_on_segment
from compas.geometry import is_point_on_segment_xy
from compas.geometry import is_point_in_triangle


def intersection_line_line(l1, l2, tol=1e-6):
    """Computes the intersection of two lines.

    Parameters
    ----------
    l1 : [point, point] | :class:`~compas.geometry.Line`
        XYZ coordinates of two points defining the first line.
    l2 : [point, point] | :class:`~compas.geometry.Line`
        XYZ coordinates of two points defining the second line.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | tuple[None, None]
        Two intersection points.
        If the lines intersect, these two points are identical.
        If the lines are skewed and thus only have an apparent intersection, the two points are different.
        In all other cases there are no intersection points.

    Examples
    --------
    The 2 intersection points of intersecting lines are identical.

    >>> l1 = [0, 0, 0], [1, 0, 0]
    >>> l2 = [0, 0, 0], [0, 1, 0]
    >>> intersection_line_line(l1, l2)
    ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0])

    Note that lines extend beyond their start and end points.

    >>> l1 = [0, 0, 0], [1, 0, 0]
    >>> l2 = [2, 0, 0], [0, 1, 0]
    >>> intersection_line_line(l1, l2)
    ([2.0, 0.0, 0.0], [2.0, 0.0, 0.0])

    Skew lines have two different intersection points.

    >>> l1 = [0, 0, 0], [1, 0, 0]
    >>> l2 = [0, 0, 1], [0, 1, 1]
    >>> intersection_line_line(l1, l2)
    ([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])

    Parallel lines don't intersect.

    >>> l1 = [0, 0, 0], [1, 0, 0]
    >>> l2 = [0, 0, 0], [1, 0, 0]
    >>> intersection_line_line(l1, l2)
    (None, None)

    """
    a, b = l1
    c, d = l2

    ab = subtract_vectors(b, a)
    cd = subtract_vectors(d, c)

    n = cross_vectors(ab, cd)
    n1 = normalize_vector(cross_vectors(ab, n))
    n2 = normalize_vector(cross_vectors(cd, n))

    plane_1 = (a, n1)
    plane_2 = (c, n2)

    i1 = intersection_line_plane(l1, plane_2, tol=tol)
    i2 = intersection_line_plane(l2, plane_1, tol=tol)

    if not i1 or not i2:
        return None, None

    return i1, i2


def intersection_segment_segment(ab, cd, tol=1e-6):
    """Compute the intersection of two lines segments.

    Parameters
    ----------
    ab : [point, point] | :class:`~compas.geometry.Line`
        XYZ coordinates of two points defining a line segment.
    cd : [point, point] | :class:`~compas.geometry.Line`
        XYZ coordinates of two points defining another line segment.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | tuple[None, None]
        Two intersection points.
        If the segments intersect and the intersection points lie on the respective segments, the two points are identical.
        If the segments are skew and the apparent intersection points lie on the respective segments, the two points are different.
        In all other cases there are no intersection points.

    Examples
    --------
    The 2 intersection points of intersecting segments are identical.

    >>> s1 = [0, 0, 0], [1, 0, 0]
    >>> s2 = [0, 0, 0], [0, 1, 0]
    >>> intersection_segment_segment(s1, s2)
    ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0])

    Unlike lines, segments don't extend beyond their start and end points.

    >>> s1 = [0, 0, 0], [1, 0, 0]
    >>> s2 = [2, 0, 0], [0, 1, 0]
    >>> intersection_segment_segment(s1, s2)
    (None, None)

    Skew segments have two different intersection points.

    >>> s1 = [0, 0, 0], [1, 0, 0]
    >>> s2 = [0, 0, 1], [0, 1, 1]
    >>> intersection_segment_segment(s1, s2)
    ([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])

    Parallel segments don't intersect.

    >>> s1 = [0, 0, 0], [1, 0, 0]
    >>> s2 = [0, 0, 0], [1, 0, 0]
    >>> intersection_segment_segment(s1, s2)
    (None, None)

    """
    x1, x2 = intersection_line_line(ab, cd, tol=tol)

    if not x1 or not x2:
        return None, None

    if not is_point_on_segment(x1, ab, tol=tol):
        return None, None

    if not is_point_on_segment(x2, cd, tol=tol):
        return None, None

    return x1, x2


def intersection_line_segment(line, segment, tol=1e-6):
    """Compute the intersection of a line and a segment.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining a line.
    segment : [point, point] | :class:`~compas.geometry.Line`
        Two points defining a line segment.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | tuple[None, None]
        Two intersection points.
        If the line and segment intersect and the second intersection point lies on the segment, the two points are identical.
        If the line and segment are skew and the second apparent intersection point lies on the segment, the two points are different.
        In all other cases there are no intersection points.

    """
    x1, x2 = intersection_line_line(line, segment, tol=tol)

    if not x1 or not x2:
        return None, None

    if not is_point_on_segment(x2, segment, tol=tol):
        return None, None

    return x1, x2


def intersection_line_plane(line, plane, tol=1e-6):
    """Computes the intersection point of a line and a plane

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the line.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal defining the plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    [float, float, float] | None
        The intersection point between the line and the plane,
        or None if the line and the plane are parallel.

    """
    a, b = line
    o, n = plane

    ab = subtract_vectors(b, a)
    cosa = dot_vectors(n, ab)

    if fabs(cosa) <= tol:
        # if the dot product (cosine of the angle between segment and plane)
        # is close to zero the line and the normal are almost perpendicular
        # hence there is no intersection
        return None

    # based on the ratio = -dot_vectors(n, ab) / dot_vectors(n, oa)
    # there are three scenarios
    # 1) 0.0 < ratio < 1.0: the intersection is between a and b
    # 2) ratio < 0.0: the intersection is on the other side of a
    # 3) ratio > 1.0: the intersection is on the other side of b
    oa = subtract_vectors(a, o)
    ratio = -dot_vectors(n, oa) / cosa
    ab = scale_vector(ab, ratio)
    return add_vectors(a, ab)


def intersection_segment_plane(segment, plane, tol=1e-6):
    """Computes the intersection point of a line segment and a plane

    Parameters
    ----------
    segment : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the line segment.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal defining the plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    [float, float, float] | None
        The intersection point between the line and the plane,
        or None if the line and the plane are parallel.

    """
    a, b = segment
    o, n = plane

    ab = subtract_vectors(b, a)
    cosa = dot_vectors(n, ab)

    if fabs(cosa) <= tol:
        # if the dot product (cosine of the angle between segment and plane)
        # is close to zero the line and the normal are almost perpendicular
        # hence there is no intersection
        return None

    # based on the ratio = -dot_vectors(n, ab) / dot_vectors(n, oa)
    # there are three scenarios
    # 1) 0.0 < ratio < 1.0: the intersection is between a and b
    # 2) ratio < 0.0: the intersection is on the other side of a
    # 3) ratio > 1.0: the intersection is on the other side of b
    oa = subtract_vectors(a, o)
    ratio = -dot_vectors(n, oa) / cosa

    if 0.0 <= ratio and ratio <= 1.0:
        ab = scale_vector(ab, ratio)
        return add_vectors(a, ab)

    return None


def intersection_polyline_plane(polyline, plane, expected_number_of_intersections=None, tol=1e-6):
    """Calculate the intersection point of a plane with a polyline. Reduce expected_number_of_intersections to speed up.

    Parameters
    ----------
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        Polyline to test intersection.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        Plane to compute intersection.
    expected_number_of_intersections : int, optional
        Number of useful or expected intersections.
        Default is the number of line segments of the polyline.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    list[[float, float, float]]
        The intersection points between the polyline segments and the plane.

    """
    if not expected_number_of_intersections:
        expected_number_of_intersections = len(polyline)
    intersections = []
    for segment in pairwise(polyline):
        if len(intersections) == expected_number_of_intersections:
            break
        point = intersection_segment_plane(segment, plane, tol)
        if point:
            intersections.append(point)
    return intersections


def intersection_line_triangle(line, triangle, tol=1e-6):
    """Computes the intersection point of a line (ray) and a triangle
    based on the Moeller Trumbore intersection algorithm

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        Two points defining the line.
    triangle : [point, point, point]
        XYZ coordinates of the triangle corners.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    [float, float, float] | None
        The intersection point between the line and the triangle,
        or None if the line and the plane are parallel.

    """
    a, b, c = triangle
    ab = subtract_vectors(b, a)
    ac = subtract_vectors(c, a)
    n = cross_vectors(ab, ac)
    plane = a, n

    x = intersection_line_plane(line, plane, tol=tol)

    if x:
        if is_point_in_triangle(x, triangle):
            return x


def intersection_plane_plane(plane1, plane2, tol=1e-6):
    """Computes the intersection of two planes

    Parameters
    ----------
    plane1 : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal (normalized) defining the 1st plane.
    plane2 : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal (normalized) defining the 2nd plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | None
        Two points defining the intersection line.
        None if the planes are parallel.

    """
    o1, n1 = plane1
    o2, n2 = plane2

    if fabs(dot_vectors(n1, n2)) >= 1 - tol:
        return None

    # direction of intersection line
    d = cross_vectors(n1, n2)
    # vector in plane 1 perpendicular to the direction of the intersection line
    v1 = cross_vectors(d, n1)
    # point on plane 1
    p1 = add_vectors(o1, v1)

    x1 = intersection_line_plane((o1, p1), plane2, tol=tol)
    x2 = add_vectors(x1, d)
    return x1, x2


def intersection_plane_plane_plane(plane1, plane2, plane3, tol=1e-6):
    """Computes the intersection of three planes

    Parameters
    ----------
    plane1 : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal (normalized) defining the 1st plane.
    plane2 : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal (normalized) defining the 2nd plane.
    plane3 : [point, vector] | :class:`~compas.geometry.Plane`
        The base point and normal (normalized) defining the 3rd plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    [float, float, float] | None
        The intersection point
        or None if at least one pair of planes is parallel.

    Notes
    -----
    Currently this only computes the intersection point.
    For example, if two planes are parallel the intersection lines are not computed [1]_.

    References
    ----------
    .. [1] http://geomalgorithms.com/Pic_3-planes.gif

    """
    line = intersection_plane_plane(plane1, plane2, tol=tol)
    if line:
        return intersection_line_plane(line, plane3, tol=tol)


def intersection_sphere_sphere(sphere1, sphere2):
    """Computes the intersection of 2 spheres.

    Parameters
    ----------
    sphere1 : [point, float] | :class:`~compas.geometry.Sphere`
        A sphere defined by a point and radius.
    sphere2 : [point, float] | :class:`~compas.geometry.Sphere`
        A sphere defined by a point and radius.

    Returns
    -------
    {'point', 'circle', or 'sphere'}
        The type of intersection.
    [float, float, float] | tuple[[float, float, float], float, [float, float, float]] | tuple[[float, float, float], float]
        If the type is 'point', the coordinates of the point.
        If the type is 'circle', the center point and radius of the circle, and the normal of the plane containing the circle.
        If the type is 'sphere', the center point and radius of the sphere.

    Notes
    -----
    There are 4 cases of sphere-sphere intersection [1]_:

    1. the spheres intersect in a circle,
    2. they intersect in a point,
    3. they overlap,
    4. they do not intersect.

    References
    ----------
    .. [1] https://gamedev.stackexchange.com/questions/75756/sphere-sphere-intersection-and-circle-sphere-intersection

    Examples
    --------
    >>> sphere1 = (3.0, 7.0, 4.0), 10.0
    >>> sphere2 = (7.0, 4.0, 0.0), 5.0
    >>> result = intersection_sphere_sphere(sphere1, sphere2)
    >>> if result:
    ...     case, res = result
    ...     if case == "circle":
    ...         center, radius, normal = res
    ...     elif case == "point":
    ...         point = res
    ...     elif case == "sphere":
    ...         center, radius = res

    """

    center1, radius1 = sphere1
    center2, radius2 = sphere2

    distance = distance_point_point(center1, center2)

    # Case 4: No intersection
    if radius1 + radius2 < distance:
        return None

    # Case 4: No intersection, sphere is within the other sphere
    elif distance + min(radius1, radius2) < max(radius1, radius2):
        return None

    # Case 3: sphere's overlap
    elif radius1 == radius2 and distance == 0:
        return "sphere", sphere1

    # Case 2: point intersection
    elif radius1 + radius2 == distance:
        ipt = subtract_vectors(center2, center1)
        ipt = scale_vector(ipt, radius1 / distance)
        ipt = add_vectors(center1, ipt)
        return "point", ipt

    # Case 2: point intersection, smaller sphere is within the bigger
    elif distance + min(radius1, radius2) == max(radius1, radius2):
        if radius1 > radius2:
            ipt = subtract_vectors(center2, center1)
            ipt = scale_vector(ipt, radius1 / distance)
            ipt = add_vectors(center1, ipt)
        else:
            ipt = subtract_vectors(center1, center2)
            ipt = scale_vector(ipt, radius2 / distance)
            ipt = add_vectors(center2, ipt)
        return "point", ipt

    # Case 1: circle intersection
    h = 0.5 + (radius1**2 - radius2**2) / (2 * distance**2)
    ci = subtract_vectors(center2, center1)
    ci = scale_vector(ci, h)
    ci = add_vectors(center1, ci)
    ri = sqrt(radius1**2 - h**2 * distance**2)
    normal = scale_vector(subtract_vectors(center2, center1), 1 / distance)
    return "circle", (ci, ri, normal)


def intersection_segment_polyline(segment, polyline, tol=1e-6):
    """Calculate the intersection point of a segment and a polyline.

    Parameters
    ----------
    segment : [point, point] | :class:`~compas.geometry.Line`
        XYZ coordinates of two points defining a line segment.
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        XYZ coordinates of the points of the polyline.
    tol : float, optional
        The tolerance for intersection verification.

    Returns
    -------
    [float, float, float] | None
        The intersection point
        or None if the segment does not intersect with any of the polyline segments.

    Examples
    --------
    >>> from compas.geometry import is_point_on_polyline
    >>> from compas.geometry import is_point_on_segment
    >>> from compas.geometry import distance_point_point
    >>> from compas.geometry import centroid_points
    >>> p = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.5), (2.0, 0.5, 1.0)]
    >>> s = [(0.5, 0.0, 0.0), (0.5, 0.0, 2.0)]
    >>> x1, x2 = intersection_segment_polyline(s, p)
    >>> x = centroid_points([x1, x2])

    >>> is_point_on_polyline(x, p)
    True

    >>> is_point_on_segment(x, s)
    True

    >>> distance_point_point((0.5, 0.0, 0.25), x) < 1e-6
    True
    """
    for cd in pairwise(polyline):
        pt = intersection_segment_segment(segment, cd, tol)
        if pt:
            return pt


def intersection_sphere_line(sphere, line):
    """Computes the intersection of a sphere and a line.

    Parameters
    ----------
    sphere : [point, radius] | :class:`~compas.geometry.Sphere`
        A sphere defined by a point and a radius.
    line : [point, point] | :class:`~compas.geometry.Line`
        A line defined by two points.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | [float, float, float] | None
        Two points (if the line goes through the sphere), one point (if the line is tangent to the sphere), or None (otherwise).

    Notes
    -----
    There are 3 cases of sphere-line intersection:

    1. they intersect in 2 points
    2. they intersect in 1 point (line tangent to sphere), or
    3. they do not intersect.

    Examples
    --------
    >>> from compas.geometry import allclose

    >>> sphere = (3.0, 7.0, 4.0), 10.0
    >>> line = (1.0, 0, 0.5), (2.0, 1.0, 0.5)
    >>> x1, x2 = intersection_sphere_line(sphere, line)

    >>> allclose(x1, [11.634, 10.634, 0.500], 1e-3)
    True
    >>> allclose(x2, [-0.634, -1.634, 0.50], 1e-3)
    True

    """
    l1, l2 = line
    sp, radius = sphere

    a = (l2[0] - l1[0]) ** 2 + (l2[1] - l1[1]) ** 2 + (l2[2] - l1[2]) ** 2
    b = 2.0 * (
        (l2[0] - l1[0]) * (l1[0] - sp[0]) + (l2[1] - l1[1]) * (l1[1] - sp[1]) + (l2[2] - l1[2]) * (l1[2] - sp[2])
    )

    c = (
        sp[0] ** 2
        + sp[1] ** 2
        + sp[2] ** 2
        + l1[0] ** 2
        + l1[1] ** 2
        + l1[2] ** 2
        - 2.0 * (sp[0] * l1[0] + sp[1] * l1[1] + sp[2] * l1[2])
        - radius**2
    )

    i = b * b - 4.0 * a * c

    if i < 0.0:  # case 3: no intersection
        return None
    elif i == 0.0:  # case 2: one intersection
        mu = -b / (2.0 * a)
        ipt = (
            l1[0] + mu * (l2[0] - l1[0]),
            l1[1] + mu * (l2[1] - l1[1]),
            l1[2] + mu * (l2[2] - l1[2]),
        )
        return ipt
    elif i > 0.0:  # case 1: two intersections
        # 1.
        mu = (-b + sqrt(i)) / (2.0 * a)
        ipt1 = (
            l1[0] + mu * (l2[0] - l1[0]),
            l1[1] + mu * (l2[1] - l1[1]),
            l1[2] + mu * (l2[2] - l1[2]),
        )
        # 2.
        mu = (-b - sqrt(i)) / (2.0 * a)
        ipt2 = (
            l1[0] + mu * (l2[0] - l1[0]),
            l1[1] + mu * (l2[1] - l1[1]),
            l1[2] + mu * (l2[2] - l1[2]),
        )
        return ipt1, ipt2


def intersection_plane_circle(plane, circle):
    """Computes the intersection of a plane and a circle.

    Parameters
    ----------
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        A plane defined by a point and normal vector.
    circle : [plane, float] | :class:`~compas.geometry.Circle`
        A circle defined by a plane and radius.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | [float, float, float] | None
        Two points (secant intersection), one point (tangent intersection), or None (otherwise).

    Notes
    -----
    There are 4 cases of plane-circle intersection:

    1. they intersect in 2 points (secant),
    2. they intersect in 1 point (tangent),
    3. they do not intersect, or
    4. they coincide (circle.plane == plane).

    Examples
    --------
    >>> plane = (0, 0, 0), (0, 0, 1)
    >>> circle = ((0, 0, 0), (0, 1, 0)), 10.0
    >>> x1, x2 = intersection_plane_circle(plane, circle)
    >>> x1
    (-10.0, 0.0, 0.0)
    >>> x2
    (10.0, 0.0, 0.0)

    """
    circle_plane, circle_radius = circle
    line = intersection_plane_plane(plane, circle_plane)
    if not line:
        return None
    circle_point = circle_plane[0]
    sphere = circle_point, circle_radius
    return intersection_sphere_line(sphere, line)


# ==============================================================================
# XY
# ==============================================================================


def intersection_line_line_xy(l1, l2, tol=1e-6):
    """Compute the intersection of two lines, assuming they lie on the XY plane.

    Parameters
    ----------
    l1 : [point, point] | :class:`~compas.geometry.Line`
        A line defined by two points, with at least XY coordinates.
    l2 : [point, point] | :class:`~compas.geometry.Line`
        A line defined by two points, with at least XY coordinates.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    [float, float, 0.0] | None
        XYZ coordinates of intersection point if one exists, with Z = 0.
        Otherwise, None.

    """
    a, b = l1
    c, d = l2

    x1, y1 = a[0], a[1]
    x2, y2 = b[0], b[1]
    x3, y3 = c[0], c[1]
    x4, y4 = d[0], d[1]

    d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)

    if fabs(d) <= tol:
        return None

    a = x1 * y2 - y1 * x2
    b = x3 * y4 - y3 * x4
    x = (a * (x3 - x4) - (x1 - x2) * b) / d
    y = (a * (y3 - y4) - (y1 - y2) * b) / d

    return [x, y, 0.0]


def intersection_line_segment_xy(line, segment, tol=1e-6):
    """Compute the intersection between a line and a segment.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        A line defined by two points, with at least XY coordinates.
    segment : [point, point] | :class:`~compas.geometry.Line`
        A segment defined by two points, with at least XY coordinates.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    [float, float, 0.0] | None
        XYZ coordinates of the intersection, if one exists, with Z = 0.
        None otherwise.

    """
    x = intersection_line_line_xy(line, segment, tol=tol)
    if x:
        if is_point_on_segment_xy(x, segment, tol=tol):
            return x


def intersection_line_box_xy(line, box, tol=1e-6):
    """Compute the intersection between a line and a box in the XY plane.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        A line defined by two points, with at least XY coordinates.
    box : [point, point, point, point]
        A box defined by 4 points, with at least XY coordinates.
    tol : float, optional
        A tolerance value for point comparison.

    Returns
    -------
    tuple[[float, float, 0.0], [float, float, 0.0]] | [float, float, 0.0] | None
        Two points if the line goes through the box.
        One point if the line goes through one of the box vertices only.
        None otherwise.

    """
    points = []
    for segment in pairwise(box + box[:1]):
        x = intersection_line_segment_xy(line, segment, tol=tol)
        if x:
            points.append(x)
    if len(points) < 3:
        return tuple(points)
    if len(points) == 3:
        a, b, c = points
        if allclose(a, b, tol=tol):
            return a, c
        if allclose(b, c, tol=tol):
            return a, b
        return b, c


def intersection_polyline_box_xy(polyline, box, tol=1e-6):
    """Compute the intersection between a polyline and a box in the XY plane.

    Parameters
    ----------
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        A polyline defined by a sequence of points, with at least XY coordinates.
    box : [point, point, point, point]
        A box defined by a sequence of 4 points, with at least XY coordinates.
    tol : float, optional
        A tolerance value for point comparison.

    Returns
    -------
    list[[float, float, 0.0]]
        A list of intersection points.

    """
    precision = compas.PRECISION
    compas.set_precision(tol)
    points = []
    for side in pairwise(box + box[:1]):
        for segment in pairwise(polyline):
            x = intersection_segment_segment_xy(side, segment, tol=tol)
            if x:
                points.append(x)
    points = {geometric_key(point): point for point in points}
    compas.PRECISION = precision
    return list(points.values())


def intersection_segment_segment_xy(ab, cd, tol=1e-6):
    """Compute the intersection of two lines segments, assuming they lie in the XY plane.

    Parameters
    ----------
    ab : [point, point] | :class:`~compas.geometry.Line`
        A segment defined by two points, with at least XY coordinates.
    cd : [point, point] | :class:`~compas.geometry.Line`
        A segment defined by two points, with at least XY coordinates.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    [float, float, 0.0] | None
        XYZ coordinates of intersection point if one exists.
        None otherwise.

    """
    intx_pt = intersection_line_line_xy(ab, cd)

    if not intx_pt:
        return None

    if not is_point_on_segment_xy(intx_pt, ab, tol=tol):
        return None

    if not is_point_on_segment_xy(intx_pt, cd, tol=tol):
        return None

    return intx_pt


def intersection_circle_circle_xy(circle1, circle2):
    """Calculates the intersection points of two circles in 2d lying in the XY plane.

    Parameters
    ----------
    circle1 : [plane, float] | :class:`~compas.geometry.Circle`
        Circle defined by a plane, with at least XY coordinates, and a radius.
    circle2 : [plane, float] | :class:`~compas.geometry.Circle`
        Circle defined by a plane, with at least XY coordinates, and a radius.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | None
        The intersection points if there are any.
        If the circles are tangent to each other, the two intersection points are identical.
        None otherwise.

    """
    plane1, r1 = circle1
    plane2, r2 = circle2
    p1, n1 = plane1
    p2, n2 = plane2
    R = length_vector_xy(subtract_vectors_xy(p2, p1))

    if R > r1 + r2:
        return None

    if R < fabs(r1 - r2):
        return None

    if (R == 0) and (r1 == r2):
        return None

    x1, y1 = p1[:2]
    x2, y2 = p2[:2]

    cx = 0.5 * (x1 + x2)
    cy = 0.5 * (y2 + y1)

    R2 = R * R
    R4 = R2 * R2

    a = (r1 * r1 - r2 * r2) / (2 * R2)
    b = 0.5 * sqrt(2 * (r1 * r1 + r2 * r2) / R2 - (r1 * r1 - r2 * r2) ** 2 / R4 - 1)

    i1 = cx + a * (x2 - x1) + b * (y2 - y1), cy + a * (y2 - y1) + b * (x1 - x2), 0
    i2 = cx + a * (x2 - x1) - b * (y2 - y1), cy + a * (y2 - y1) - b * (x1 - x2), 0

    return i1, i2


def intersection_segment_polyline_xy(segment, polyline, tol=1e-6):
    """
    Calculate the intersection point of a segment and a polyline on the XY-plane.

    Parameters
    ----------
    segment : [point, point] | :class:`~compas.geometry.Line`
        A line segment defined by two points, with at least XY coordinates.
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        A polyline defined by a sequence of points, with at least XY coordinates.
    tol : float, optional
        The tolerance for intersection verification.

    Returns
    -------
    [float, float, 0.0] | None
        XYZ coordinates of the first intersection point if one exists.
        None otherwise

    Examples
    --------
    >>> from compas.geometry import is_point_on_polyline_xy
    >>> from compas.geometry import is_point_on_segment_xy
    >>> from compas.geometry import distance_point_point
    >>> p = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2.0, 0.0, 0.0)]
    >>> s = [(0.5, -0.5, 0.0), (0.5, 0.5, 0.0)]
    >>> x = intersection_segment_polyline_xy(s, p)
    >>> is_point_on_polyline_xy(x, p)
    True
    >>> is_point_on_segment_xy(x, s)
    True
    >>> distance_point_point((0.5, 0.0, 0.0), x) < 1e-6
    True

    """
    for cd in pairwise(polyline):
        pt = intersection_segment_segment_xy(segment, cd, tol)
        if pt:
            return pt


def intersection_ellipse_line_xy(ellipse, line):
    """Computes the intersection of an ellipse and a line in the XY plane.

    Parameters
    ----------
    ellipse : tuple[float, float]
        The major and minor of the ellipse.
    line : [point, point] | :class:`~compas.geometry.Line`
        A line defined by two points, with at least XY coordinates.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]] | [float, float, float] | None
        Two points, if the line goes through the ellipse.
        One point, if the line is tangent to the ellipse.
        None, otherwise.

    References
    ----------
    Based on [1]_.

    .. [1] C# Helper. *Calculate where a line segment and an ellipse intersect in C#*.
           Available at: http://csharphelper.com/blog/2017/08/calculate-where-a-line-segment-and-an-ellipse-intersect-in-c/

    Examples
    --------
    >>> ellipse = 6., 2.5
    >>> p1 = (4.1, 2.8, 0.)
    >>> p2 = (3.4, -3.1, 0.)
    >>> i1, i2 = intersection_ellipse_line_xy(ellipse, [p1, p2])

    """
    x1, y1 = line[0][0], line[0][1]
    x2, y2 = line[1][0], line[1][1]

    a, b = ellipse

    A = (x2 - x1) ** 2 / a**2 + (y2 - y1) ** 2 / b**2
    B = 2 * x1 * (x2 - x1) / a**2 + 2 * y1 * (y2 - y1) / b**2
    C = x1**2 / a**2 + y1**2 / b**2 - 1

    discriminant = B**2 - 4 * A * C
    if discriminant == 0:
        t = -B / (2 * A)
        return (x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, 0.0)
    elif discriminant > 0:
        t1 = (-B + sqrt(discriminant)) / (2 * A)
        t2 = (-B - sqrt(discriminant)) / (2 * A)
        p1 = (x1 + (x2 - x1) * t1, y1 + (y2 - y1) * t1, 0.0)
        p2 = (x1 + (x2 - x1) * t2, y1 + (y2 - y1) * t2, 0.0)
        return p1, p2
    else:
        return None


# def intersection_line_circle_xy(line, circle):
#     """Compute the intersection of a line and a circle in the XY plane.

#     Parameters
#     ----------

#     """
#     x0, y0 = circle[0][:2]
#     r = circle[1]
#     x1, y1 = line[0][:2]
#     x2, y2 = line[1][:2]
#     a = y1 - y2
#     b = x2 - x1
#     c = x1 * y2 - x2 * y1
#     D = sqrt(r**2 - c**2 / (a**2 + b**2))
#     m = sqrt(D**2 / (a**2 + b**2))
#     p1 = [x0 + b * m, y0 - a * m, 0]
#     p2 = [x0 - b * m, y0 + a * m, 0]
#     return p1, p2


# src/compas/geometry/offset/offset.py

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

from compas.geometry import scale_vector
from compas.geometry import normalize_vector
from compas.geometry import add_vectors
from compas.geometry import subtract_vectors
from compas.geometry import cross_vectors
from compas.geometry import centroid_points
from compas.geometry import intersection_line_line
from compas.geometry import normal_polygon
from compas.geometry import is_colinear

from compas.data import is_item_iterable

from compas.utilities import iterable_like
from compas.utilities import pairwise


def intersect_lines(l1, l2, tol):
    x1, x2 = intersection_line_line(l1, l2, tol)
    if x1 and x2:
        return centroid_points([x1, x2])


def intersect_lines_colinear(l1, l2, tol):
    def are_segments_colinear(l1, l2, tol):
        a, b = l1
        d, c = l2
        return is_colinear(a, b, c, tol)

    if are_segments_colinear(l1, l2, tol):
        return centroid_points([l1[1], l2[0]])


def intersect(l1, l2, tol):
    supported_funcs = [intersect_lines, intersect_lines_colinear]
    for func in supported_funcs:
        point = func(l1, l2, tol)
        if point:
            return point
    msg = "Intersection not found for line: {}, and line: {}".format(l1, l2)
    raise ValueError(msg)


def offset_segments(point_list, distances, normal):
    segments = []
    for line, distance in zip(pairwise(point_list), distances):
        segments.append(offset_line(line, distance, normal))
    return segments


def offset_line(line, distance, normal=[0.0, 0.0, 1.0]):
    """Offset a line by a distance.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        A line defined by two points.
    distances : float or list[float]
        The offset distance as float.
        A single value determines a constant offset.
        A list of two offset values can be used to a create variable offset at the start and end.
    normal : [float, float, float] | :class:`~compas.geometry.Vector`, optional
        The normal of the offset plane.

    Returns
    -------
    tuple[[float, float, float], [float, float, float]]
        Two points defining the offset line.

    Notes
    -----
    The offset direction is chosen such that if the line were along the positve
    X axis and the normal of the offset plane is along the positive Z axis, the
    offset line is in the direction of the postive Y axis.

    Examples
    --------
    >>>

    """

    a, b = line
    ab = subtract_vectors(b, a)
    direction = normalize_vector(cross_vectors(normal, ab))

    if not is_item_iterable(distance):
        distance = [distance]
    distances = list(iterable_like(line, distance, distance[-1]))

    u = scale_vector(direction, distances[0])
    v = scale_vector(direction, distances[1])
    c = add_vectors(a, u)
    d = add_vectors(b, v)
    return c, d


def offset_polygon(polygon, distance, tol=1e-6):
    """Offset a polygon (closed) by a distance.

    Parameters
    ----------
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        The XYZ coordinates of the corners of the polygon.
        The first and last coordinates must not be identical.
    distance : float | list[tuple[float, float]]
        The offset distance as float.
        A single value determines a constant offset globally.
        A list of pairs of local offset values per line segment can be used to create variable offsets.
    tol : float, optional
        A tolerance value for intersection calculations.

    Returns
    -------
    list[[float, float, float]]
        The XYZ coordinates of the corners of the offset polygon.
        The first and last coordinates are identical.

    Notes
    -----
    The offset direction is determined by the normal of the polygon.
    If the polygon is in the XY plane and the normal is along the positive Z axis,
    positive offset distances will result in an offset towards the inside of the
    polygon.

    The algorithm works also for spatial polygons that do not perfectly fit a plane.

    Examples
    --------
    >>> from compas.geometry import Polygon
    >>> polygon = Polygon([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])
    >>> offsetted_polygon = offset_polygon(polygon, 0.5)
    >>> offsetted_polygon
    Polygon[[0.5, 0.5, 0.0], [0.5, 0.5, 0.0], [0.5, 0.5, 0.0], [0.5, 0.5, 0.0]]

    """
    normal = normal_polygon(polygon)

    if not is_item_iterable(distance):
        distance = [distance]
    distances = iterable_like(polygon, distance, distance[-1])

    polygon = polygon[:] + polygon[:1]
    segments = offset_segments(polygon, distances, normal)

    offset = []
    for s1, s2 in pairwise(segments[-1:] + segments):
        point = intersect(s1, s2, tol)
        offset.append(point)

    return offset


def offset_polyline(polyline, distance, normal=[0.0, 0.0, 1.0], tol=1e-6):
    """Offset a polyline by a distance.

    Parameters
    ----------
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        The XYZ coordinates of the vertices of a polyline.
    distance : float | list[tuple[float, float]]
        The offset distance as float.
        A single value determines a constant offset globally.
        Alternatively, pairs of local offset values per line segment can be used to create variable offsets.
    normal : [float, float, float] | :class:`~compas.geometry.Vector`, optional
        The normal of the offset plane.
    tol : float, optional
        A tolerance value for intersection calculations.

    Returns
    -------
    list[[float, float, float]]
        The XYZ coordinates of the resulting polyline.

    Notes
    -----
    The offset direction is determined by the provided normal vector.
    If the polyline is in the XY plane and the normal is along the positive Z axis,
    positive offset distances will result in counterclockwise offsets,
    and negative values in clockwise direction.

    Examples
    --------
    >>>

    """

    if not is_item_iterable(distance):
        distance = [distance]
    distances = iterable_like(polyline, distance, distance[-1])
    segments = offset_segments(polyline, distances, normal)

    offset = [segments[0][0]]
    for s1, s2 in pairwise(segments):
        point = intersect(s1, s2, tol)
        offset.append(point)
    offset.append(segments[-1][1])

    return offset


# src/compas/geometry/predicates/predicates_2.py

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

from compas.geometry import distance_point_point_xy
from compas.geometry import distance_point_line_xy
from compas.geometry import closest_point_on_segment_xy


def is_ccw_xy(a, b, c, colinear=False):
    """Determine if c is on the left of ab when looking from a to b,
    and assuming that all points lie in the XY plane.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        Base point defined by XY(Z) coordinates.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        First end point defined by XY(Z) coordinates.
    c : [float, float, float] | :class:`~compas.geometry.Point`
        Second end point defined by XY(Z) coordinates.
    colinear : bool, optional
        If True, colinear points will return a positive result.

    Returns
    -------
    bool
        True if ccw.
        False otherwise.

    References
    ----------
    For more info, see [1]_.

    .. [1] Marsh, C. *Computational Geometry in Python: From Theory to Application*.
           Available at: https://www.toptal.com/python/computational-geometry-in-python-from-theory-to-implementation

    Examples
    --------
    >>> print(is_ccw_xy([0,0,0], [0,1,0], [-1, 0, 0]))
    True

    >>> print(is_ccw_xy([0,0,0], [0,1,0], [+1, 0, 0]))
    False

    >>> print(is_ccw_xy([0,0,0], [1,0,0], [2,0,0]))
    False

    >>> print(is_ccw_xy([0,0,0], [1,0,0], [2,0,0], True))
    True

    """
    ab_x = b[0] - a[0]
    ab_y = b[1] - a[1]
    ac_x = c[0] - a[0]
    ac_y = c[1] - a[1]
    if colinear:
        return ab_x * ac_y - ab_y * ac_x >= 0
    return ab_x * ac_y - ab_y * ac_x > 0


def is_colinear_xy(a, b, c):
    """Determine if three points are colinear on the XY-plane.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        Point 1 defined by XY(Z) coordinates.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        Point 2 defined by XY(Z) coordinates.
    c : [float, float, float] | :class:`~compas.geometry.Point`
        Point 3 defined by XY(Z) coordinates.

    Returns
    -------
    bool
        True if the points are colinear.
        False otherwise.

    """
    ab_x = b[0] - a[0]
    ab_y = b[1] - a[1]
    ac_x = c[0] - a[0]
    ac_y = c[1] - a[1]
    return ab_x * ac_y == ab_y * ac_x


def is_polygon_convex_xy(polygon, colinear=False):
    """Determine if the polygon is convex on the XY-plane.

    Parameters
    ----------
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        The XY(Z) coordinates of the corners of a polygon.
        The vertices are assumed to be in order.
        The polygon is assumed to be closed: the first and last vertex in the sequence should not be the same.
    colinear : bool, optional
        Are points allowed to be colinear?

    Returns
    -------
    bool
        True if the polygon is convex.
        False otherwise.

    """
    a = polygon[-2]
    b = polygon[-1]
    c = polygon[0]
    direction = is_ccw_xy(a, b, c, colinear)
    for i in range(-1, len(polygon) - 2):
        a = b
        b = c
        c = polygon[i + 2]
        if direction != is_ccw_xy(a, b, c, colinear):
            return False
    return True


def is_point_on_line_xy(point, line, tol=1e-6):
    """Determine if a point lies on a line on the XY-plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point.
    line : [point, point] | :class:`~compas.geometry.Line`
        XY(Z) coordinates of two points defining a line.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is in on the line.
        False otherwise.

    """
    return distance_point_line_xy(point, line) <= tol


def is_point_on_segment_xy(point, segment, tol=1e-6):
    """Determine if a point lies on a given line segment on the XY-plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point.
    segment : [point, point] | :class:`~compas.geometry.Line`
        XY(Z) coordinates of two points defining a segment.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is on the line segment.
        False otherwise.

    """
    a, b = segment

    if not is_point_on_line_xy(point, segment, tol=tol):
        return False

    d_ab = distance_point_point_xy(a, b)

    if d_ab == 0:
        return False

    d_pa = distance_point_point_xy(a, point)
    d_pb = distance_point_point_xy(b, point)

    if d_pa + d_pb <= d_ab + tol:
        return True

    return False


def is_point_on_polyline_xy(point, polyline, tol=1e-6):
    """Determine if a point is on a polyline on the XY-plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates.
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        XY(Z) coordinates of the points of the polyline.
    tol : float, optional
        The tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is on the polyline.
        False otherwise.

    """
    for i in range(len(polyline) - 1):
        a = polyline[i]
        b = polyline[i + 1]
        c = closest_point_on_segment_xy(point, (a, b))

        if distance_point_point_xy(point, c) <= tol:
            return True

    return False


def is_point_in_triangle_xy(point, triangle, colinear=False):
    """Determine if a point is in the interior of a triangle lying on the XY-plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point.
    triangle : [point, point, point]
        XY(Z) coordinates of the corners of the triangle.
    colinear : bool, optional
        Allow points to be colinear.

    Returns
    -------
    bool
        True if the point is in the convex polygon.
        False otherwise.

    """
    a, b, c = triangle
    ccw = is_ccw_xy(c, a, point, colinear)

    if ccw != is_ccw_xy(a, b, point, colinear):
        return False

    if ccw != is_ccw_xy(b, c, point, colinear):
        return False

    return True


def is_point_in_convex_polygon_xy(point, polygon):
    """Determine if a point is in the interior of a convex polygon lying on the XY-plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point (Z will be ignored).
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        A sequence of XY(Z) coordinates of points representing the locations of the corners of a polygon (Z will be ignored).
        The vertices are assumed to be in order. The polygon is assumed to be closed:
        the first and last vertex in the sequence should not be the same.

    Returns
    -------
    bool
        True if the point is in the convex polygon
        False otherwise.

    Warnings
    --------
    Does not work for concave polygons.

    """
    ccw = None
    for i in range(-1, len(polygon) - 1):
        a = polygon[i]
        b = polygon[i + 1]
        if ccw is None:
            ccw = is_ccw_xy(a, b, point, True)
        else:
            if ccw != is_ccw_xy(a, b, point, True):
                return False
    return True


def is_point_in_polygon_xy(point, polygon):
    """Determine if a point is in the interior of a polygon lying on the XY-plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point (Z will be ignored).
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        A sequence of XY(Z) coordinates of points representing the locations of the corners of a polygon (Z will be ignored).
        The vertices are assumed to be in order.
        The polygon is assumed to be closed.
        The first and last vertex in the sequence should not be the same.

    Returns
    -------
    bool
        True if the point is in the polygon.
        False otherwise.

    Warnings
    --------
    A boundary check is not yet implemented. This should include a tolerance value.

    """
    x, y = point[0], point[1]
    polygon = [(p[0], p[1]) for p in polygon]  # make 2D
    inside = False
    for i in range(-1, len(polygon) - 1):
        x1, y1 = polygon[i]
        x2, y2 = polygon[i + 1]
        if y1 != y2:
            xinters = (y - y1) * (x2 - x1) / (y2 - y1) + x1
        else:
            xinters = None
        if y > min(y1, y2):
            if y <= max(y1, y2):
                if x <= max(x1, x2):
                    if x1 == x2 or (xinters is not None and x <= xinters):
                        inside = not inside
    return inside


def is_point_in_circle_xy(point, circle):
    """Determine if a point lies in a circle lying on the XY-plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        XY(Z) coordinates of a point (Z will be ignored).
    circle : [[point, vector], float] | :class:`~compas.geometry.Circle`
        Center and radius of the circle on the XY plane.

    Returns
    -------
    bool
        True if the point lies in the circle.
        False otherwise.

    """
    dis = distance_point_point_xy(point, circle[0][0])
    if dis <= circle[1]:
        return True
    return False


def is_polygon_in_polygon_xy(polygon1, polygon2):
    """Determine if a polygon is in the interior of another polygon on the XY-plane.

    Parameters
    ----------
    polygon1 : sequence[point] | :class:`~compas.geometry.Polygon`
        List of XY(Z) coordinates of points representing the locations of the corners of the exterior polygon (Z will be ignored).
        The vertices are assumed to be in order. The polygon is assumed to be closed:
        the first and last vertex in the sequence should not be the same.
    polygon2 : sequence[point] | :class:`~compas.geometry.Polygon`
        List of XY(Z) coordinates of points representing the locations of the corners of the interior polygon (Z will be ignored).
        The vertices are assumed to be in order. The polygon is assumed to be closed:
        the first and last vertex in the sequence should not be the same.

    Returns
    -------
    bool
        True if polygon2 is inside polygon1.
        False otherwise.

    """
    if is_polygon_convex_xy(polygon1) and is_polygon_convex_xy(polygon2):
        for pt in polygon2:
            if not is_point_in_convex_polygon_xy(pt, polygon1):
                return False
        return True
    else:
        for i in range(len(polygon1)):
            line = [polygon1[-i], polygon1[-i - 1]]
            for j in range(len(polygon2)):
                line_ = [polygon2[-j], polygon2[-j - 1]]
                if is_intersection_segment_segment_xy(line, line_):
                    return False
        for pt in polygon2:
            if is_point_in_polygon_xy(pt, polygon1):
                return True
        return False


def is_intersection_line_line_xy(l1, l2, tol=1e-6):
    """Verifies if two lines intersect on the XY-plane.

    Parameters
    ----------
    l1 : [point, point] | :class:`~compas.geometry.Line`
        XY(Z) coordinates of two points defining a line.
    l2 : [point, point] | :class:`~compas.geometry.Line`
        XY(Z) coordinates of two points defining a line.
    tol : float, optional
        A tolerance for intersection verification.

    Returns
    -------
    bool
        True if the lines intersect in one point
        False if the lines are skew, parallel or lie on top of each other.

    """
    raise NotImplementedError


def is_intersection_segment_segment_xy(ab, cd):
    """Determines if two segments, ab and cd, intersect.

    Parameters
    ----------
    ab : [point, point] | :class:`~compas.geometry.Line`
        Two points representing the start and end points of a segment.
        Z coordinates will be ignored.
    cd : [point, point] | :class:`~compas.geometry.Line`
        Two points representing the start and end points of a segment.
        Z coordinates will be ignored.

    Returns
    -------
    bool
        True if the segments intersect.
        False otherwise.

    Notes
    -----
    The segments intersect if both of the following conditions are true:

    * `c` is on the left of `ab`, and `d` is on the right, or vice versa.
    * `d` is on the left of `ac`, and on the right of `bc`, or vice versa.

    """
    a, b = ab
    c, d = cd
    return is_ccw_xy(a, c, d) != is_ccw_xy(b, c, d) and is_ccw_xy(a, b, c) != is_ccw_xy(a, b, d)


# src/compas/geometry/predicates/predicates_3.py

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

from math import fabs

from compas.utilities import window

from compas.geometry import subtract_vectors
from compas.geometry import cross_vectors
from compas.geometry import dot_vectors
from compas.geometry import normalize_vector
from compas.geometry import centroid_points
from compas.geometry import normal_polygon
from compas.geometry import length_vector_sqrd

from compas.geometry import distance_point_point
from compas.geometry import distance_point_plane
from compas.geometry import distance_point_line
from compas.geometry import closest_point_on_segment

from compas.geometry import area_triangle


def is_colinear(a, b, c, tol=1e-6):
    """Determine if three points are colinear.

    Parameters
    ----------
    a : [float, float, float] | :class:`~compas.geometry.Point`
        Point 1.
    b : [float, float, float] | :class:`~compas.geometry.Point`
        Point 2.
    c : [float, float, float] | :class:`~compas.geometry.Point`
        Point 3.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the points are colinear.
        False otherwise.

    """
    return area_triangle([a, b, c]) < tol


def is_colinear_line_line(line1, line2, tol=1e-6):
    """Determine if two lines are colinear.

    Parameters
    ----------
    line1 : [point, point] | :class:`~compas.geometry.Line`
        Line 1.
    line2 : [point, point] | :class:`~compas.geometry.Line`
        Line 2.
    tol : float, optional
        A tolerance for colinearity verification.

    Returns
    -------
    bool
        True if the lines are colinear.
        False otherwise.

    """
    a, b = line1
    c, d = line2
    return is_colinear(a, b, c, tol) and is_colinear(a, b, d, tol)


def is_parallel_line_line(line1, line2, tol=1e-6):
    """Determine if two lines are parallel.

    Parameters
    ----------
    line1 : [point, point] | :class:`~compas.geometry.Line`
        Line 1.
    line2 : [point, point] | :class:`~compas.geometry.Line`
        Line 2.
    tol : float, optional
        A tolerance for colinearity verification.

    Returns
    -------
    bool
        True if the lines are colinear.
        False otherwise.

    """
    a, b = line1
    c, d = line2
    e1 = normalize_vector(subtract_vectors(b, a))
    e2 = normalize_vector(subtract_vectors(d, c))
    return abs(dot_vectors(e1, e2)) > 1.0 - tol


def is_coplanar(points, tol=0.01):
    """Determine if the points are coplanar.

    Parameters
    ----------
    points : sequence[point]
        A sequence of point locations.
    tol : float, optional
        A tolerance for planarity validation.

    Returns
    -------
    bool
        True if the points are coplanar.
        False otherwise.

    Notes
    -----
    Compute the normal vector (cross product) of the vectors formed by the first
    three points. Include one more vector at a time to compute a new normal and
    compare with the original normal. If their cross product is not zero, they
    are not parallel, which means the point are not in the same plane.

    Four points are coplanar if the volume of the tetrahedron defined by them is
    0. Coplanarity is equivalent to the statement that the pair of lines
    determined by the four points are not skew, and can be equivalently stated
    in vector form as (x2 - x0).[(x1 - x0) x (x3 - x2)] = 0.

    """
    if len(points) < 4:
        return True

    tol2 = tol**2

    if len(points) == 4:
        v01 = subtract_vectors(points[1], points[0])
        v02 = subtract_vectors(points[2], points[0])
        v23 = subtract_vectors(points[3], points[2])
        res = dot_vectors(v02, cross_vectors(v01, v23))
        return res**2 < tol2

    a, b, c = points[:3]
    ab = subtract_vectors(b, a)
    n0 = cross_vectors(ab, subtract_vectors(c, a))
    points = points[3:]
    for c in points:
        n1 = cross_vectors(ab, subtract_vectors(c, a))
        if length_vector_sqrd(cross_vectors(n0, n1)) > tol:
            return False
    return True


def is_polygon_convex(polygon):
    """Determine if a polygon is convex.

    Parameters
    ----------
    polygon : sequence[point] | :class:`~compas.geometry.Polygon`
        A polygon.

    Returns
    -------
    bool
        True if the polygon is convex.
        False otherwise.

    Notes
    -----
    Use this function for *spatial* polygons.
    If the polygon is in a horizontal plane, use :func:`is_polygon_convex_xy` instead.

    Examples
    --------
    >>> polygon = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.4, 0.4, 0.0], [0.0, 1.0, 0.0]]
    >>> is_polygon_convex(polygon)
    False

    """
    a = polygon[0]
    o = polygon[1]
    b = polygon[2]
    oa = subtract_vectors(a, o)
    ob = subtract_vectors(b, o)
    n0 = cross_vectors(oa, ob)
    for a, o, b in window(polygon + polygon[:2], 3):
        oa = subtract_vectors(a, o)
        ob = subtract_vectors(b, o)
        n = cross_vectors(oa, ob)
        if dot_vectors(n, n0) >= 0:
            continue
        else:
            return False
    return True


def is_point_on_plane(point, plane, tol=1e-6):
    """Determine if a point lies on a plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        A plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is in on the plane.
        False otherwise.

    """
    return distance_point_plane(point, plane) <= tol


def is_point_infront_plane(point, plane, tol=1e-6):
    """Determine if a point lies in front of a plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        A plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is in front of the plane.
        False otherwise.

    """
    return dot_vectors(subtract_vectors(point, plane[0]), plane[1]) > tol


is_point_in_halfspace = is_point_infront_plane


def is_point_behind_plane(point, plane, tol=1e-6):
    """Determine if a point lies behind a plane.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    plane : [point,  normal] | :class:`~compas.geometry.Plane`
        A plane.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is in front of the plane.
        False otherwise.

    """
    return dot_vectors(subtract_vectors(point, plane[0]), plane[1]) < -tol


def is_point_on_line(point, line, tol=1e-6):
    """Determine if a point lies on a line.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    line : [point, point] | :class:`~compas.geometry.Line`
        A line.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is in on the line.
        False otherwise.

    """
    return distance_point_line(point, line) <= tol


def is_point_on_segment(point, segment, tol=1e-6):
    """Determine if a point lies on a given line segment.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    segment : [point, point] | :class:`~compas.geometry.Line`
        A line segment.
    tol : float, optional
        A tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is on the line segment.
        False otherwise.

    """
    a, b = segment

    d_ab = distance_point_point(a, b)
    if d_ab == 0:
        return False

    if not is_point_on_line(point, (a, b), tol=tol):
        return False

    d_pa = distance_point_point(a, point)
    d_pb = distance_point_point(b, point)

    if d_pa + d_pb <= d_ab + tol:
        return True
    return False


def is_point_on_polyline(point, polyline, tol=1e-6):
    """Determine if a point is on a polyline.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    polyline : sequence[point] | :class:`~compas.geometry.Polyline`
        A polyline.
    tol : float, optional
        The tolerance for membership verification.

    Returns
    -------
    bool
        True if the point is on the polyline.
        False otherwise.

    """
    for i in range(len(polyline) - 1):
        a = polyline[i]
        b = polyline[i + 1]
        c = closest_point_on_segment(point, (a, b))

        if distance_point_point(point, c) <= tol:
            return True

    return False


def is_point_in_triangle(point, triangle):
    """Determine if a point is in the interior of a triangle.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    triangle : [point, point, point]
        A triangle.

    Returns
    -------
    bool
        True if the point is in inside the triangle.
        False otherwise.

    See Also
    --------
    compas.geometry.is_point_in_triangle_xy

    Notes
    -----
    Should the point be on the same plane as the triangle?

    """

    def is_on_same_side(p1, p2, segment):
        a, b = segment
        v = subtract_vectors(b, a)
        c1 = cross_vectors(v, subtract_vectors(p1, a))
        c2 = cross_vectors(v, subtract_vectors(p2, a))
        if dot_vectors(c1, c2) >= 0:
            return True
        return False

    a, b, c = triangle

    if is_on_same_side(point, a, (b, c)) and is_on_same_side(point, b, (a, c)) and is_on_same_side(point, c, (a, b)):
        return True

    return False


def is_point_in_circle(point, circle):
    """Determine if a point lies in a circle.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        A point.
    circle : [plane, float] | :class:`~compas.geometry.Circle`
        A circle.

    Returns
    -------
    bool
        True if the point lies in the circle.
        False otherwise.

    """
    plane, radius = circle
    if is_point_on_plane(point, plane):
        return distance_point_point(point, plane[0]) <= radius
    return False


def is_intersection_line_line(l1, l2, tol=1e-6):
    """Verifies if two lines intersect.

    Parameters
    ----------
    l1 : [point, point] | :class:`~compas.geometry.Line`
        A line.
    l2 : [point, point] | :class:`~compas.geometry.Line`
        A line.
    tol : float, optional
        A tolerance for intersection verification.

    Returns
    -------
    bool
        True if the lines intersect in one point.
        False if the lines are skew, parallel or lie on top of each other.

    """
    a, b = l1
    c, d = l2

    e1 = normalize_vector(subtract_vectors(b, a))
    e2 = normalize_vector(subtract_vectors(d, c))

    # check for parallel lines
    if abs(dot_vectors(e1, e2)) > 1.0 - tol:
        return False

    # check for intersection
    if abs(dot_vectors(cross_vectors(e1, e2), subtract_vectors(c, a))) < tol:
        return True
    return False


def is_intersection_segment_segment(s1, s2, tol=1e-6):
    """Verifies if two segments intersect.

    Parameters
    ----------
    s1 : [point, point] | :class:`~compas.geometry.Line`
        A line segment.
    s2 : [point, point] | :class:`~compas.geometry.Line`
        A line segment.
    tol : float, optional
        A tolerance for intersection verification.

    Returns
    -------
    bool
        True if the segments intersect in one point.
        False if the segments are skew, parallel or lie on top of each other.

    """
    raise NotImplementedError


def is_intersection_line_triangle(line, triangle, tol=1e-6):
    """Verifies if a line (ray) intersects with a triangle.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        A line.
    triangle : [point, point, point]
        A triangle.
    tol : float, optional
        A tolerance for intersection verification.

    Returns
    -------
    bool
        True if the line (ray) intersects with the triangle.
        False otherwise.

    Notes
    -----
    Based on the Moeller Trumbore intersection algorithm.
    The line is treated as continues, directed ray and not as line segment with a start and end point

    Examples
    --------
    >>>

    """
    a, b, c = triangle
    # direction vector and base point of line
    v1 = subtract_vectors(line[1], line[0])
    p1 = line[0]
    # Find vectors for two edges sharing triangle vertex 1
    e1 = subtract_vectors(b, a)
    e2 = subtract_vectors(c, a)
    # Begin calculating determinant - also used to calculate u parameter
    p = cross_vectors(v1, e2)
    # if determinant is near zero, ray lies in plane of triangle
    det = dot_vectors(e1, p)

    # NOT CULLING
    if det > -tol and det < tol:
        return False

    inv_det = 1.0 / det
    # calculate distance from V1 to ray origin
    t = subtract_vectors(p1, a)
    # Calculate u parameter and make_blocks bound
    u = dot_vectors(t, p) * inv_det

    # The intersection lies outside of the triangle
    if u < 0.0 or u > 1.0:
        return False

    # Prepare to make_blocks v parameter
    q = cross_vectors(t, e1)
    # Calculate V parameter and make_blocks bound
    v = dot_vectors(v1, q) * inv_det

    # The intersection lies outside of the triangle
    if v < 0.0 or u + v > 1.0:
        return False

    t = dot_vectors(e2, q) * inv_det

    if t > tol:
        return True
    # No hit
    return False


def is_intersection_line_plane(line, plane, tol=1e-6):
    """Determine if a line (ray) intersects with a plane.

    Parameters
    ----------
    line : [point, point] | :class:`~compas.geometry.Line`
        A line.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        A plane.
    tol : float, optional
        A tolerance for intersection verification.

    Returns
    -------
    bool
        True if the line intersects with the plane.
        False otherwise.

    """
    pt1 = line[0]
    pt2 = line[1]
    p_norm = plane[1]

    v1 = subtract_vectors(pt2, pt1)
    dot = dot_vectors(p_norm, v1)

    if fabs(dot) > tol:
        return True
    return False


def is_intersection_segment_plane(segment, plane, tol=1e-6):
    """Determine if a line segment intersects with a plane.

    Parameters
    ----------
    segment : [point, point] | :class:`~compas.geometry.Line`
        A line segment.
    plane : [point, vector] | :class:`~compas.geometry.Plane`
        A plane.
    tol : float, optional
        A tolerance for intersection verification.

    Returns
    -------
    bool
        True if the segment intersects with the plane.
        False otherwise.

    """
    pt1 = segment[0]
    pt2 = segment[1]
    p_cent = plane[0]
    p_norm = plane[1]

    v1 = subtract_vectors(pt2, pt1)
    dot = dot_vectors(p_norm, v1)

    if fabs(dot) > tol:
        v2 = subtract_vectors(pt1, p_cent)
        fac = -dot_vectors(p_norm, v2) / dot
        if fac > 0.0 and fac < 1.0:
            return True
        return False
    else:
        return False


def is_intersection_plane_plane(plane1, plane2, tol=1e-6):
    """Verifies if two planes intersect.

    Parameters
    ----------
    plane1 : [point, vector] | :class:`~compas.geometry.Plane`
        A plane.
    plane2 : [point, vector] | :class:`~compas.geometry.Plane`
        A plane.
    tol : float, optional
        A tolerance for intersection verification.

    Returns
    -------
    bool
        True if plane1 intersects with plane2.
        False otherwise.

    """
    # check for parallelity of planes
    if abs(dot_vectors(plane1[1], plane2[1])) > 1 - tol:
        return False
    return True


def is_point_in_box(point, box):
    """Determine if the point lies inside the given box.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        The test point.
    box : sequence[point] | :class:`~compas.geometry.Box`.
        The box defined by 8 points with the first 4 points defining the bottom face,
        and the last 4 points defining the top face.

    Returns
    -------
    bool
        True, if the point lies inside the box.
        False, otherwise.

    """
    raise NotImplementedError


def is_point_in_polyhedron(point, polyhedron):
    """Determine if the point lies inside the given polyhedron.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        The test point.
    polyhedron : [sequence[point], sequence[sequence[int]]] | :class:`~compas.geometry.Polyhedron`.
        The polyhedron defined by a sequence of points
        and a sequence of faces, with each face defined as a sequence of indices into the sequence of points.

    Returns
    -------
    bool
        True, if the point lies in the polyhedron.
        False, otherwise.

    """
    vertices, faces = polyhedron
    polygons = [[vertices[index] for index in face] for face in faces]
    planes = [[centroid_points(polygon), normal_polygon(polygon)] for polygon in polygons]
    return all(is_point_behind_plane(point, plane) for plane in planes)


# src/compas/geometry/quadmesh/planarization.py

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

from compas.plugins import pluggable


@pluggable(category="quadmesh")
def quadmesh_planarize(M, kmax=500, maxdev=0.005):
    """Planarize the faces of a quad mesh.

    Parameters
    ----------
    M : [sequence[[float, float, float]], sequence[[int, int, int, int]]]
        A quad mesh represented by a list of vertices and a list of faces.
    kmax : int, optional
        The maximum number of iterations.
    maxdev : float, optional
        The maximum deviation from planar.

    Returns
    -------
    list
        The coordinates of the new vertices.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/shapes/box.py

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

from compas.geometry import centroid_points
from compas.geometry import transform_points
from compas.geometry import Transformation
from compas.geometry import Frame
from compas.geometry import Vector
from compas.geometry import Line

from .shape import Shape


class Box(Shape):
    """A box is defined by a frame and its dimensions along the frame's x-, y- and z-axes.

    The center of the box is positioned at the origin of the
    coordinate system defined by the frame. The box is axis-aligned to the frame.

    A box is a three-dimensional geometric shape with 8 vertices, 12 edges and 6
    faces. The edges of a box meet at its vertices at 90 degree angles. The
    faces of a box are planar. Faces which do not share an edge are parallel.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`
        The frame of the box.
    xsize : float
        The size of the box in the box frame's x direction.
    ysize : float
        The size of the box in the box frame's y direction.
    zsize : float
        The size of the box in the box frame's z direction.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The box's frame.
    xsize : float
        The size of the box in the box frame's x direction.
    ysize : float
        The size of the box in the box frame's y direction.
    zsize : float
        The size of the box in the box frame's z direction.
    xmin : float, read-only
        Minimum value along local X axis.
    xmax : float, read-only
        Maximum value along local X axis.
    ymin : float, read-only
        Minimum value along local Y axis.
    ymax : float, read-only
        Maximum value along local Y axis.
    zmin : float, read-only
        Minimum value along local Z axis.
    zmax : float, read-only
        Maximum value along local Z axis.
    width : float, read-only
        The width of the box in X direction.
    depth : float, read-only
        The depth of the box in Y direction.
    height : float, read-only
        The height of the box in Z direction.
    diagonal : :class:`~compas.geometry.Line`, read-only
        Diagonal of the box.
    dimensions : list[float], read-only
        The dimensions of the box in the local frame.
    area : float, read-only
        The surface area of the box.
    volume : float, read-only
        The volume of the box.
    points : list[:class:`~compas.geometry.Point`], read-only
        The XYZ coordinates of the corners of the box.
    vertices : list[:class:`~compas.geometry.Point`], read-only
        The XYZ coordinates of the vertices of the box.
    faces : list[list[int]], read-only
        The faces of the box defined as lists of vertex indices.
    bottom : list[int], read-only
        The vertex indices of the bottom face.
    front : list[int], read-only
        The vertex indices of the front face.
    right : list[int], read-only
        The vertex indices of the right face.
    back : list[int], read-only
        The vertex indices of the back face.
    left : list[int], read-only
        The vertex indices of the left face.
    top : list[int], read-only
        The vertex indices of the top face.
    edges : list[tuple[int, int]], read-only
        The edges of the box as vertex index pairs.

    Examples
    --------
    >>> box = Box(Frame.worldXY(), 1.0, 2.0, 3.0)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "xsize": {"type": "number", "exclusiveMinimum": 0},
            "ysize": {"type": "number", "exclusiveMinimum": 0},
            "zsize": {"type": "number", "exclusiveMinimum": 0},
        },
        "additionalProperties": False,
        "minProperties": 4,
    }

    def __init__(self, frame=None, xsize=1.0, ysize=1.0, zsize=1.0, size=1.0, **kwargs):
        super(Box, self).__init__(frame=frame, **kwargs)
        self._xsize = None
        self._ysize = None
        self._zsize = None
        self.xsize = xsize or size
        self.ysize = ysize or size
        self.zsize = zsize or size

    def __repr__(self):
        return "Box({0!r}, {1!r}, {2!r}, {3!r})".format(self.frame, self.xsize, self.ysize, self.zsize)

    def __len__(self):
        return 4

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.xsize
        elif key == 2:
            return self.ysize
        elif key == 3:
            return self.zsize
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.xsize = value
        elif key == 2:
            self.ysize = value
        elif key == 3:
            self.zsize = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.xsize, self.ysize, self.zsize])

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {
            "frame": self.frame,
            "xsize": self.xsize,
            "ysize": self.ysize,
            "zsize": self.zsize,
        }

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.xsize = data["xsize"]
        self.ysize = data["ysize"]
        self.zsize = data["zsize"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def xsize(self):
        if not self._xsize:
            raise ValueError("The size of the box along the local X axis is not set.")
        return self._xsize

    @xsize.setter
    def xsize(self, xsize):
        if xsize <= 0:
            raise ValueError("The size of the box along the local X axis should be larger than zero.")
        self._xsize = float(xsize)

    @property
    def ysize(self):
        if not self._ysize:
            raise ValueError("The size of the box along the local Y axis is not set.")
        return self._ysize

    @ysize.setter
    def ysize(self, ysize):
        if ysize <= 0:
            raise ValueError("The size of the box along the local Y axis should be larger than zero.")
        self._ysize = float(ysize)

    @property
    def zsize(self):
        if not self._zsize:
            raise ValueError("The size of the box along the local Z axis is not set.")
        return self._zsize

    @zsize.setter
    def zsize(self, zsize):
        if zsize <= 0:
            raise ValueError("The size of the box along the local Z axis should be larger than zero.")
        self._zsize = float(zsize)

    @property
    def xmin(self):
        return self.frame.point.x - 0.5 * self.xsize

    @property
    def xmax(self):
        return self.frame.point.x + 0.5 * self.xsize

    @property
    def ymin(self):
        return self.frame.point.y - 0.5 * self.ysize

    @property
    def ymax(self):
        return self.frame.point.y + 0.5 * self.ysize

    @property
    def zmin(self):
        return self.frame.point.z - 0.5 * self.zsize

    @property
    def zmax(self):
        return self.frame.point.z + 0.5 * self.zsize

    @property
    def width(self):
        return self.xsize

    @property
    def depth(self):
        return self.ysize

    @property
    def height(self):
        return self.zsize

    @property
    def diagonal(self):
        vertices = self.vertices
        return Line(vertices[0], vertices[-2])

    @property
    def dimensions(self):
        return [self.xsize, self.ysize, self.zsize]

    @property
    def area(self):
        return 2 * self.xsize * self.ysize + 2 * self.ysize * self.zsize + 2 * self.zsize * self.xsize

    @property
    def volume(self):
        return self.xsize * self.ysize * self.zsize

    @property
    def points(self):
        return self.vertices

    @property
    def vertices(self):
        point = self.frame.point
        xaxis = self.frame.xaxis
        yaxis = self.frame.yaxis
        zaxis = self.frame.zaxis

        dx = 0.5 * self.xsize
        dy = 0.5 * self.ysize
        dz = 0.5 * self.zsize

        a = point + xaxis * -dx + yaxis * -dy + zaxis * -dz
        b = point + xaxis * -dx + yaxis * +dy + zaxis * -dz
        c = point + xaxis * +dx + yaxis * +dy + zaxis * -dz
        d = point + xaxis * +dx + yaxis * -dy + zaxis * -dz
        e = a + zaxis * self.zsize
        f = d + zaxis * self.zsize
        g = c + zaxis * self.zsize
        h = b + zaxis * self.zsize

        return [a, b, c, d, e, f, g, h]

    @property
    def faces(self):
        return [self.bottom, self.front, self.right, self.back, self.left, self.top]

    @property
    def bottom(self):
        return [0, 1, 2, 3]

    @property
    def front(self):
        return [0, 3, 5, 4]

    @property
    def right(self):
        return [3, 2, 6, 5]

    @property
    def back(self):
        return [2, 1, 7, 6]

    @property
    def left(self):
        return [1, 0, 4, 7]

    @property
    def top(self):
        return [4, 5, 6, 7]

    @property
    def edges(self):
        edges = [(0, 1), (1, 2), (2, 3), (3, 0)]
        edges += [(4, 5), (5, 6), (6, 7), (7, 4)]
        edges += [(0, 4), (1, 7), (2, 6), (3, 5)]
        return edges

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_width_height_depth(cls, width, height, depth):
        """Construct a box from its width, height and depth.

        Note that width is along the X-axis, height along Z-axis, and depth along the Y-axis.

        Parameters
        ----------
        width : float
            Width of the box.
        height : float
            Height of the box.
        depth : float
            Depth of the box.

        Returns
        -------
        :class:`~compas.geometry.Box`
            The resulting box.

        Notes
        -----
        The box is axis-aligned to the world coordinate system and centered at the origin.

        Examples
        --------
        >>> box = Box.from_width_height_depth(1.0, 2.0, 3.0)

        """
        width = float(width)
        height = float(height)
        depth = float(depth)

        if width == 0.0:
            raise Exception("Width cannot be zero.")

        if height == 0.0:
            raise Exception("Height cannot be zero.")

        if depth == 0.0:
            raise Exception("Depth cannot be zero.")

        return cls(Frame.worldXY(), width, depth, height)

    @classmethod
    def from_bounding_box(cls, bbox):
        """Construct a box from the result of a bounding box calculation.

        Parameters
        ----------
        bbox : list[[float, float, float] | :class:`~compas.geometry.Point`]
            A list of 8 point locations, representing the corners of the bounding box.
            Positions 0, 1, 2, 3 are the bottom corners.
            Positions 4, 5, 6, 7 are the top corners.
            Both the top and bottom face are oriented in CCW direction, starting at the bottom, left-most point.

        Returns
        -------
        :class:`~compas.geometry.Box`
            The box shape.

        Examples
        --------
        >>> from compas.geometry import bounding_box
        >>> bbox = bounding_box([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
        >>> box = Box.from_bounding_box(bbox)
        >>> box.width
        1.0
        >>> box.height
        1.0
        >>> box.depth
        1.0

        """
        a = bbox[0]
        b = bbox[1]
        d = bbox[3]
        e = bbox[4]
        xaxis = Vector.from_start_end(a, b)
        yaxis = Vector.from_start_end(a, d)
        zaxis = Vector.from_start_end(a, e)
        xsize = xaxis.length
        ysize = yaxis.length
        zsize = zaxis.length
        frame = Frame(centroid_points(bbox), xaxis, yaxis)
        return cls(frame=frame, xsize=xsize, ysize=ysize, zsize=zsize)

    @classmethod
    def from_corner_corner_height(cls, corner1, corner2, height):
        """Construct a box from the opposite corners of its base and its height.

        Parameters
        ----------
        corner1 : [float, float, float] | :class:`~compas.geometry.Point`
            The XYZ coordinates of the bottom left corner of the base of the box.
        corner2 : [float, float, float] | :class:`~compas.geometry.Point`
            The XYZ coordinates of the top right corner of the base of the box.
        height : float
            The height of the box.

        Returns
        -------
        :class:`~compas.geometry.Box`
            The resulting box.

        Examples
        --------
        >>> box = Box.from_corner_corner_height([0.0, 0.0, 0.0], [1.0, 1.0, 0.0], 1.0)

        """
        if height == 0:
            raise Exception("The box should have a height.")

        x1, y1, z1 = corner1
        x2, y2, z2 = corner2

        if z1 != z2:
            raise Exception("Corners should be in the same horizontal plane.")

        xaxis = Vector(x2 - x1, 0, 0)
        yaxis = Vector(0, y2 - y1, 0)
        width = xaxis.length
        depth = yaxis.length
        point = [0.5 * (x1 + x2), 0.5 * (y1 + y2), z1 + 0.5 * height]
        frame = Frame(point, xaxis, yaxis)

        return cls(frame=frame, xsize=width, ysize=depth, zsize=height)

    @classmethod
    def from_diagonal(cls, diagonal):
        """Construct a box from its main diagonal.

        Parameters
        ----------
        diagonal : [point, point] | :class:`~compas.geometry.Line`
            The diagonal of the box, represented by a pair of points in space.

        Returns
        -------
        :class:`~compas.geometry.Box`
            The resulting box.

        Examples
        --------
        >>> diagonal = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
        >>> box = Box.from_diagonal(diagonal)

        """
        d1, d2 = diagonal

        x1, y1, z1 = d1
        x2, y2, z2 = d2

        if z1 == z2:
            raise Exception("The box has no height.")

        xaxis = Vector(x2 - x1, 0, 0)
        yaxis = Vector(0, y2 - y1, 0)
        zaxis = Vector(0, 0, z2 - z1)
        width = xaxis.length
        depth = yaxis.length
        height = zaxis.length
        point = [0.5 * (x1 + x2), 0.5 * (y1 + y2), 0.5 * (z1 + z2)]
        frame = Frame(point, xaxis, yaxis)

        return cls(frame, width, depth, height)

    # ==========================================================================
    # Transformations
    # ==========================================================================

    # def transform(self, transformation):
    #     """Transform the box.

    #     Parameters
    #     ----------
    #     transformation : :class:`Transformation`
    #         The transformation used to transform the Box.

    #     Returns
    #     -------
    #     None

    #     Examples
    #     --------
    #     >>> box = Box(Frame.worldXY(), 1.0, 2.0, 3.0)
    #     >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    #     >>> T = Transformation.from_frame(frame)
    #     >>> box.transform(T)

    #     """
    #     self.frame.transform(transformation)
    #     # Always local scaling, non-uniform scaling based on frame not yet considered.
    #     Sc, _, _, _, _ = transformation.decomposed()
    #     scalex = Sc[0, 0]
    #     scaley = Sc[1, 1]
    #     scalez = Sc[2, 2]
    #     self.xsize *= scalex
    #     self.ysize *= scaley
    #     self.zsize *= scalez

    def scale(self, factor):
        """Scale the box.

        Parameters
        ----------
        factor : float
            The scaling factor.

        Returns
        -------
        None

        """
        self.xsize *= factor
        self.ysize *= factor
        self.zsize *= factor

    # ==========================================================================
    # Methods
    # ==========================================================================

    def to_vertices_and_faces(self, triangulated=False):
        """Returns a list of vertices and faces.

        Parameters
        ----------
        triangulated: bool, optional
            If True, triangulate the faces.

        Returns
        -------
        list[list[float]], list[list[int]]
            A list of vertex locations, and a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if triangulated:
            faces = []
            for a, b, c, d in self.faces:
                faces.append([a, b, c])
                faces.append([a, c, d])
        else:
            faces = self.faces
        return self.vertices, faces

    def contains_point(self, point, tol=1e-6):
        """Verify if the box contains a given point.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            The point to test.
        tol : float, optional
            The tolerance for the point containment check.

        Returns
        -------
        bool

        """
        T = Transformation.from_change_of_basis(Frame.worldXY(), self.frame)
        x, y, z = transform_points([point], T)[0]

        dx = 0.5 * self.xsize + tol

        if x < -dx or x > +dx:
            return False

        dy = 0.5 * self.ysize + tol

        if y < -dy or y > +dy:
            return False

        dz = 0.5 * self.zsize + tol

        if z < -dz or z > +dz:
            return False

        return True

    def contains_points(self, points, tol=1e-6):
        """Verify if the box contains the given points.

        Parameters
        ----------
        points : list[[float, float, float]] | list[:class:`~compas.geometry.Point`]
            A list of points.
        tol : float, optional
            The tolerance for the point containment check.

        Returns
        -------
        list[bool]

        Examples
        --------
        >>> from compas.geometry import Point, Box
        >>> box = Box(Frame.worldXY(), 2.0, 2.0, 2.0)
        >>> points = [Point(0.0, 0.0, 0.0), Point(1.0, 1.0, 1.0)]
        >>> results = box.contains_points(points)
        >>> all(results)
        True

        """
        dx = 0.5 * self.xsize + tol
        dy = 0.5 * self.ysize + tol
        dz = 0.5 * self.zsize + tol

        T = Transformation.from_change_of_basis(Frame.worldXY(), self.frame)
        points = transform_points(points, T)
        results = [False] * len(points)

        for index, (x, y, z) in enumerate(points):
            if x < -dx or x > +dx:
                continue

            if y < -dy or y > +dy:
                continue

            if z < -dz or z > +dz:
                continue

            results[index] = True

        return results


# src/compas/geometry/shapes/capsule.py

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

from math import cos
from math import pi
from math import sin

from compas.geometry import transform_points
from compas.geometry import Frame
from compas.geometry import Plane
from compas.geometry import Line
from compas.geometry import Circle
from compas.geometry import Transformation

from .shape import Shape


class Capsule(Shape):
    """A capsule is defined by a frame, radius, and height.

    The capsule is oriented along the z-axis of the frame.
    The base point (i.e. the centre of the base circle) is at the origin of the frame.
    Half of the capsule is above the local XY plane of the frame, the other half below.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`, optional
        The local coordinate system, or "frame", of the capsule.
        Default is ``None``, in which case the world coordinate system is used.
    radius : float, optional
        The radius of the capsule.
    height : float, optional
        The height of the capsule along the z-axis of the frame.
        Half of the capsule is above the XY plane of the frame, the other half below.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The local coordinate system of the capsule.
        The capsule is oriented along the local z-axis.
    transformation : :class:`~compas.geometry.Transformation`
        The transformation of the capsule to global coordinates.
    radius : float
        The radius of the base circle of the capsule.
    height : float
        The height of the capsule.
    axis : :class:`~compas.geometry.Line`, read-only
        The central axis of the capsule.
    base : :class:`~compas.geometry.Point`, read-only
        The base point of the capsule.
        The base point is at the origin of the local coordinate system.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the capsule.
        The base point of the plane is at the origin of the local coordinate system.
        The normal of the plane is in the direction of the z-axis of the local coordinate system.
    circle : :class:`~compas.geometry.Circle`, read-only
        The base circle of the capsule.
        The center of the circle is at the origin of the local coordinate system.
    diameter : float, read-only
        The diameter of the base circle of the capsule.
    area : float, read-only
        The surface area of the capsule.
    volume : float, read-only
        The volume of the capsule.

    Examples
    --------
    >>> frame = Frame.worldXY()
    >>> capsule = Capsule(frame=frame, radius=0.3, heigth=1.0)
    >>> capsule = Capsule(radius=0.3, heigth=1.0)
    >>> capsule = Capsule()

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "radius": {"type": "number", "minimum": 0},
            "height": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "radius", "height"],
    }

    def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs):
        super(Capsule, self).__init__(frame=frame, **kwargs)
        self._radius = None
        self._height = None
        self.radius = radius
        self.height = height

    def __repr__(self):
        return "Capsule(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.radius
        elif key == 2:
            return self.height
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.radius = value
        elif key == 2:
            self.height = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.radius, self.height])

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "radius": self.radius, "height": self.height}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.radius = data["radius"]
        self.height = data["height"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("The radius of the capsule is not set.")
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("The radius of the capsule should be greater than or equal to zero.")
        self._radius = float(radius)

    @property
    def height(self):
        if self._height is None:
            raise ValueError("The capsule has no height.")
        return self._height

    @height.setter
    def height(self, height):
        if height < 0:
            raise ValueError("The height of the capsule should be greater than or equal to zero.")
        self._height = float((height))

    @property
    def length(self):
        return self.height

    @property
    def start(self):
        return self.frame.point - self.frame.zaxis * self.height / 2

    @property
    def end(self):
        return self.frame.point + self.frame.zaxis * self.height / 2

    @property
    def axis(self):
        return Line(self.start, self.end)

    @property
    def circle(self):
        return Circle(self.frame, self.radius)

    @property
    def volume(self):
        # cylinder plus 2 half spheres
        cylinder = self.radius**2 * pi * self.height
        caps = 4.0 / 3.0 * pi * self.radius**3
        return cylinder + caps

    @property
    def area(self):
        # cylinder minus caps plus 2 half spheres
        cylinder = self.radius * 2 * pi * self.height
        caps = 4 * pi * self.radius**2
        return cylinder + caps

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_line_and_radius(cls, line, radius):
        """Construct a capsule from a line and a radius.

        Parameters
        ----------
        line : :class:`~compas.geometry.Line`
            The line.
        radius : float
            The radius.

        Returns
        -------
        :class:`~compas.geometry.Capsule`
            The constructed capsule.

        See Also
        --------
        :meth:`Capsule.from_circle_and_height`

        Examples
        --------
        >>> line = Line(Point(0, 0, 0), Point(0, 0, 1))
        >>> capsule = Capsule.from_line_and_radius(line, 0.3)

        """
        frame = Frame.from_plane(Plane(line.midpoint, line.direction))
        return cls(frame=frame, radius=radius, height=line.length)

    @classmethod
    def from_circle_and_height(cls, circle, height):
        """Construct a capsule from a circle and a height.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`
            The circle.
        height : float
            The height.

        Returns
        -------
        :class:`~compas.geometry.Capsule`
            The constructed capsule.

        See Also
        --------
        :meth:`Capsule.from_line_and_radius`

        Examples
        --------
        >>> circle = Circle(Frame.worldXY(), 0.3)
        >>> capsule = Capsule.from_circle_and_height(circle, 1.0)

        """
        return cls(frame=circle.frame, radius=circle.radius, height=height)

    # =============================================================================
    # Conversions
    # =============================================================================

    def to_vertices_and_faces(self, u=16, v=16, triangulated=False):
        """Returns a list of vertices and faces.

        Note that the vertex coordinates are defined with respect to the global coordinate system,
        and not to the local coordinate system of the capsule.

        Parameters
        ----------
        u : int, optional
            Number of faces in the 'u' direction.
        v : int, optional
            Number of faces in the 'v' direction.
        triangulated: bool, optional
            If True, triangulate the faces.

        Returns
        -------
        list[list[float]], list[list[int]]
            A list of vertex locations, and a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if u < 3:
            raise ValueError("The value for u should be u > 3.")
        if v < 3:
            raise ValueError("The value for v should be v > 3.")
        if v % 2 == 1:
            v += 1

        theta = pi / v
        phi = pi * 2 / u
        hpi = pi * 0.5
        halfheight = self.height / 2
        sidemult = -1
        capswitch = 0

        vertices = []
        for i in range(1, v + 1):
            for j in range(u):
                a = i + capswitch
                tx = self.radius * cos(a * theta - hpi) * cos(j * phi)
                ty = self.radius * cos(a * theta - hpi) * sin(j * phi)
                tz = self.radius * sin(a * theta - hpi) + sidemult * halfheight
                vertices.append([tx, ty, tz])
            # switch from lower pole cap to upper pole cap
            if i == v / 2 and sidemult == -1:
                capswitch = -1
                sidemult *= -1

        vertices.append([0, 0, halfheight + self.radius])
        vertices.append([0, 0, -halfheight - self.radius])

        faces = []

        # south pole triangle fan
        sp = len(vertices) - 1
        for j in range(u):
            faces.append([sp, (j + 1) % u, j])

        for i in range(v - 1):
            for j in range(u):
                jj = (j + 1) % u
                a = i * u + j
                b = i * u + jj
                c = (i + 1) * u + jj
                d = (i + 1) * u + j
                faces.append([a, b, c, d])

        # north pole triangle fan
        np = len(vertices) - 2
        for j in range(u):
            nc = len(vertices) - 3 - j
            nn = len(vertices) - 3 - (j + 1) % u
            faces.append([np, nn, nc])

        if triangulated:
            triangles = []
            for face in faces:
                if len(face) == 4:
                    triangles.append(face[0:3])
                    triangles.append([face[0], face[2], face[3]])
                else:
                    triangles.append(face)
            faces = triangles

        vertices = transform_points(vertices, self.transformation)

        return vertices, faces

    # =============================================================================
    # Transformations
    # =============================================================================

    def scale(self, factor):
        """Scale the capsule.

        Parameters
        ----------
        factor : float
            The scaling factor.

        Returns
        -------
        None

        """
        self.radius *= factor
        self.height *= factor

    # =============================================================================
    # Methods
    # =============================================================================

    def contains_point(self, point, tol=1e-6):
        """Verify if a point is inside the capsule.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The point.
        tol : float, optional
            The tolerance for the test.

        Returns
        -------
        bool
            True if the point is inside the capsule.
            False otherwise.

        """
        T = Transformation.from_change_of_basis(Frame.worldXY(), self.frame)
        x, y, z = transform_points([point], T)[0]
        h = 0.5 * self.height + tol

        if -h <= z <= +h:
            if x**2 + y**2 > (self.radius + tol) ** 2:
                return False

        if z > +h:
            x0, y0, z0 = self.end
            dx = x - x0
            dy = y - y0
            dz = z - z0
            if dx**2 + dy**2 + dz**2 > (self.radius + tol) ** 2:
                return False

        if z < -h:
            x0, y0, z0 = self.start
            dx = x - x0
            dy = y - y0
            dz = z - z0
            if dx**2 + dy**2 + dz**2 > (self.radius + tol) ** 2:
                return False

        return True

    def contains_points(self, points, tol=1e-6):
        """Verify if a list of points is inside the capsule.

        Parameters
        ----------
        points : list of :class:`~compas.geometry.Point`
            The points.
        tol : float, optional
            The tolerance for the test.

        Returns
        -------
        list of bool
            For each point, True if the point is inside the capsule.
            False otherwise.

        """
        T = Transformation.from_change_of_basis(Frame.worldXY(), self.frame)
        points = transform_points(points, T)

        h = 0.5 * self.height + tol
        r2 = (self.radius + tol) ** 2
        results = [False] * len(points)

        for i, (x, y, z) in enumerate(points):
            if -h <= z <= +h:
                if x**2 + y**2 > r2:
                    continue

            if z > +h:
                x0, y0, z0 = self.end
                dx = x - x0
                dy = y - y0
                dz = z - z0
                if dx**2 + dy**2 + dz**2 > r2:
                    continue

            if z < -h:
                x0, y0, z0 = self.start
                dx = x - x0
                dy = y - y0
                dz = z - z0
                if dx**2 + dy**2 + dz**2 > r2:
                    continue

            results[i] = True

        return results


# src/compas/geometry/shapes/cone.py

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

from math import cos
from math import pi
from math import sin
from math import sqrt

from compas.utilities import pairwise
from compas.geometry import transform_points
from compas.geometry import Circle
from compas.geometry import Plane
from compas.geometry import Line
from compas.geometry import Frame

from .shape import Shape


class Cone(Shape):
    """A cone is defined by a frame, radius, and height.

    The cone is oriented along the z-axis of the frame.
    The base point of the cone (i.e. the centre of the base circle) is at the origin of the frame.
    The entire cone is in the positive z-direction.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`, optional
        The local coordinate system of the cone.
        Default is ``None``, in which case the world coordinate system is used.
    radius : float, optional
        The radius of the base of the cone.
    height : float, optional
        The height of the cone along the z-axis of the frame.
        The base of the cone is at the origin of the frame.
        The entire cone is above the XY plane of the frame.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The local coordinate system of the cone.
        The cone is oriented along the local z-axis.
    transformation : :class:`~compas.geometry.Transformation`
        The transformation of the cone to global coordinates.
    radius : float
        The radius of the base circle of the cone.
    height : float
        The height of the cone.
    axis : :class:`~compas.geometry.Line`, read-only
        The central axis of the cone.
    base : :class:`~compas.geometry.Point`, read-only
        The base point of the cone.
        The base point is at the origin of the local coordinate system.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the cone.
        The base point of the plane is at the origin of the local coordinate system.
        The normal of the plane is in the direction of the z-axis of the local coordinate system.
    circle : :class:`~compas.geometry.Circle`, read-only
        The base circle of the cone.
        The center of the circle is at the origin of the local coordinate system.
    diameter : float, read-only
        The diameter of the base circle of the cone.
    area : float, read-only
        The surface area of the cone.
    volume : float, read-only
        The volume of the cone.

    Examples
    --------
    >>> frame = Frame.worldXY()
    >>> cone = Cone(frame=frame, radius=0.3, height=1.0)
    >>> cone = Cone(radius=0.3, height=1.0)
    >>> cone = Cone()

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "radius": {"type": "number", "minimum": 0},
            "height": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "radius", "height"],
    }

    def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs):
        super(Cone, self).__init__(frame=frame, **kwargs)
        self._radius = None
        self._height = None
        self.radius = radius
        self.height = height

    def __repr__(self):
        return "Cone(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.radius
        if key == 2:
            return self.height
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.radius = value
        elif key == 2:
            self.height = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.radius, self.height])

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "radius": self.radius, "height": self.height}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.radius = data["radius"]
        self.height = data["height"]

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("The cone radius has not been set.")
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("The cone radius should be larger than or equal to zero.")
        self._radius = float(radius)

    @property
    def height(self):
        if self._height is None:
            raise ValueError("The cone height has not been set.")
        return self._height

    @height.setter
    def height(self, height):
        if height < 0:
            raise ValueError("The cone height should be larger than or equal to zero.")
        self._height = float(height)

    @property
    def axis(self):
        return Line(self.frame.point, self.frame.point + self.frame.normal * self.height)

    @property
    def circle(self):
        return Circle(self.frame, self.radius)

    @property
    def diameter(self):
        return 2 * self.radius

    @property
    def area(self):
        r = self.radius
        h = self.height
        return pi * r * (r + sqrt(h**2 + r**2))

    @property
    def volume(self):
        r = self.radius
        h = self.height
        return pi * r**2 * (h / 3)

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_line_and_radius(cls, line, radius):
        """Construct a cone from a line and a radius.

        Parameters
        ----------
        line : :class:`~compas.geometry.Line`
            The axis of the cone.
        radius : float
            The radius of the base circle of the cone.

        Returns
        -------
        :class:`~compas.geometry.Cone`
            The cone.

        See Also
        --------
        :meth:`Cone.from_circle_and_height`

        Examples
        --------
        >>> line = Line(Point(0, 0, 0), Point(0, 0, 1))
        >>> cone = Cone.from_line_and_radius(line, 0.3)

        """
        frame = Frame.from_plane(Plane(line.midpoint, line.direction))
        return cls(frame=frame, radius=radius, height=line.length)

    @classmethod
    def from_circle_and_height(cls, circle, height):
        """Construct a cone from a circle and a height.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`
            The base circle of the cone.
        height : float
            The height of the cone.

        Returns
        -------
        :class:`~compas.geometry.Cone`
            The cone.

        See Also
        --------
        :meth:`Cone.from_line_and_radius`

        Examples
        --------
        >>> circle = Circle(Frame.worldXY(), 0.3)
        >>> cone = Cone.from_circle_and_height(circle, 1.0)

        """
        frame = circle.frame
        return cls(frame=frame, radius=circle.radius, height=height)

    # ==========================================================================
    # Conversions
    # ==========================================================================

    def to_vertices_and_faces(self, u=16, triangulated=False):
        """Returns a list of vertices and faces.

        Parameters
        ----------
        u : int, optional
            Number of faces in the "u" direction.
        triangulated: bool, optional
            If True, triangulate the faces.

        Returns
        -------
        list[list[float]], list[list[int]]
            A list of vertex locations, and a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if u < 3:
            raise ValueError("The value for u should be u > 3.")

        vertices = [[0, 0, 0]]
        a = 2 * pi / u
        radius = self.circle.radius
        for i in range(u):
            x = radius * cos(i * a)
            y = radius * sin(i * a)
            vertices.append([x, y, 0])  # type: ignore
        vertices.append([0, 0, self.height])  # type: ignore

        faces = []
        first = 0
        last = len(vertices) - 1
        for i, j in pairwise(range(1, last)):
            faces.append([i, j, last])
            faces.append([j, i, first])
        faces.append([last - 1, 1, last])
        faces.append([1, last - 1, first])

        if triangulated:
            triangles = []
            for face in faces:
                if len(face) == 4:
                    triangles.append(face[0:3])
                    triangles.append([face[0], face[2], face[3]])
                else:
                    triangles.append(face)
            faces = triangles

        vertices = transform_points(vertices, self.transformation)

        return vertices, faces

    # ==========================================================================
    # Transformations
    # ==========================================================================

    # ==========================================================================
    # Methods
    # ==========================================================================


# src/compas/geometry/shapes/cylinder.py

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

from math import cos
from math import pi
from math import sin

from compas.geometry import transform_points
from compas.geometry import Circle
from compas.geometry import Plane
from compas.geometry import Frame
from compas.geometry import Line

from .shape import Shape


class Cylinder(Shape):
    """A cylinder is defined by a frame, radius, and height.

    The cylinder is oriented along the z-axis of the frame.
    The base point of the cylinder (i.e. the centre of the base circle) is at the origin of the frame.
    Half of the cylinder is above the local XY plane of the frame, the other half below.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`, optional
        The local coordinate system, or "frame", of the cylinder.
        Default is ``None``, in which case the world coordinate system is used.
    radius : float, optional
        The radius of the cylinder.
    height : float, optional
        The height of the cylinder along the z-axis of the frame.
        Half of the cylinder is above the XY plane of the frame, the other half below.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The local coordinate system of the cylinder.
        The cylinder is oriented along the local z-axis.
    transformation : :class:`~compas.geometry.Transformation`
        The transformation of the cylinder to global coordinates.
    radius : float
        The radius of the base circle of the cylinder.
    height : float
        The height of the cylinder.
    axis : :class:`~compas.geometry.Line`, read-only
        The central axis of the cylinder.
    base : :class:`~compas.geometry.Point`, read-only
        The base point of the cylinder.
        The base point is at the origin of the local coordinate system.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the cylinder.
        The base point of the plane is at the origin of the local coordinate system.
        The normal of the plane is in the direction of the z-axis of the local coordinate system.
    circle : :class:`~compas.geometry.Circle`, read-only
        The base circle of the cylinder.
        The center of the circle is at the origin of the local coordinate system.
    diameter : float, read-only
        The diameter of the base circle of the cylinder.
    area : float, read-only
        The surface area of the cylinder.
    volume : float, read-only
        The volume of the cylinder.

    Examples
    --------
    >>> frame = Frame.worldXY()
    >>> cylinder = Cylinder(frame=frame, radius=0.3, heigth=1.0)
    >>> cylinder = Cylinder(radius=0.3, heigth=1.0)
    >>> cylinder = Cylinder()

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "radius": {"type": "number", "minimum": 0},
            "height": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "radius", "height"],
    }

    def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs):
        super(Cylinder, self).__init__(frame=frame, **kwargs)
        self._radius = None
        self._height = None
        self.radius = radius
        self.height = height

    def __repr__(self):
        return "Cylinder(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.radius
        elif key == 2:
            return self.height
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.radius = value
        elif key == 2:
            self.height = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.radius, self.height])

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "radius": self.radius, "height": self.height}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.radius = data["radius"]
        self.height = data["height"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("The cylinder radius has not been set.")
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("The cylinder radius should be larger than or equal to zero.")
        self._radius = float(radius)

    @property
    def height(self):
        if self._height is None:
            raise ValueError("The cylinder height has not been set.")
        return self._height

    @height.setter
    def height(self, height):
        if height < 0:
            raise ValueError("The cylinder height should be larger than or equal to zero.")
        self._height = float(height)

    @property
    def axis(self):
        return Line(self.frame.point, self.frame.point + self.frame.normal * self.height)

    @property
    def base(self):
        return self.frame.point.copy()

    @property
    def plane(self):
        return Plane(self.frame.point, self.frame.normal)

    @property
    def circle(self):
        return Circle(self.frame, self.radius)

    @property
    def diameter(self):
        return 2 * self.radius

    @property
    def area(self):
        return (self.circle.area * 2) + (self.circle.circumference * self.height)

    @property
    def volume(self):
        return self.circle.area * self.height

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_line_and_radius(cls, line, radius):
        """Construct a cylinder from a line and a radius.

        Parameters
        ----------
        line : :class:`~compas.geometry.Line`
            The line.
        radius : float
            The radius.

        Returns
        -------
        :class:`~compas.geometry.Cylinder`
            The cylinder.

        Examples
        --------
        >>> from compas.geometry import Line
        >>> from compas.geometry import Cylinder
        >>> line = Line([0, 0, 0], [0, 0, 1])
        >>> cylinder = Cylinder.from_line_and_radius(line, radius=0.3)

        """
        frame = Frame.from_plane(Plane(line.midpoint, line.direction))
        return cls(frame=frame, height=line.length, radius=radius)

    @classmethod
    def from_circle_and_height(cls, circle, height):
        """Construct a cylinder from a circle and a height.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`
            The circle.
        height : float
            The height.

        Returns
        -------
        :class:`~compas.geometry.Cylinder`
            The cylinder.

        Examples
        --------
        >>> from compas.geometry import Circle
        >>> from compas.geometry import Cylinder
        >>> circle = Circle(radius=0.3)
        >>> cylinder = Cylinder.from_circle_and_height(circle, height=1.0)

        """
        return cls(frame=circle.frame, height=height, radius=circle.radius)

    # =============================================================================
    # Conversions
    # =============================================================================

    def to_vertices_and_faces(self, u=16, triangulated=False):
        """Returns a list of vertices and faces.

        Parameters
        ----------
        u : int, optional
            Number of faces in the "u" direction.
        triangulated: bool, optional
            If True, triangulate the faces.

        Returns
        -------
        list[list[float]]
            A list of vertex locations.
        list[list[int]]
            And a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if u < 3:
            raise ValueError("The value for u should be u > 3.")

        vertices = []
        a = 2 * pi / u
        z = self.height / 2
        for i in range(u):
            x = self.circle.radius * cos(i * a)
            y = self.circle.radius * sin(i * a)
            vertices.append([x, y, z])
            vertices.append([x, y, -z])
        # add v in bottom and top's circle center
        vertices.append([0, 0, z])
        vertices.append([0, 0, -z])

        faces = []
        # side faces
        for i in range(0, u * 2, 2):
            faces.append([i, i + 1, (i + 3) % (u * 2), (i + 2) % (u * 2)])
        # top and bottom circle faces
        for i in range(0, u * 2, 2):
            top = [i, (i + 2) % (u * 2), len(vertices) - 2]
            bottom = [i + 1, (i + 3) % (u * 2), len(vertices) - 1]
            faces.append(top)
            faces.append(bottom[::-1])

        if triangulated:
            triangles = []
            for face in faces:
                if len(face) == 4:
                    triangles.append(face[0:3])
                    triangles.append([face[0], face[2], face[3]])
                else:
                    triangles.append(face)
            faces = triangles

        vertices = transform_points(vertices, self.transformation)

        return vertices, faces

    # =============================================================================
    # Transformations
    # =============================================================================

    def scale(self, factor):
        """Scale the cylinder by multiplying the radius and height by a factor.

        Parameters
        ----------
        factor : float
            The scaling factor.

        Returns
        -------
        None

        """
        self.radius *= factor
        self.height *= factor

    # =============================================================================
    # Methods
    # =============================================================================

    def contains_point(self, point, tol=1e-6):
        """Verify if a point is inside the cylinder.

        Parameters
        ----------
        point : :class:`compas.geometry.Point`
            The point.
        tol : float, optional
            The tolerance for the verification.

        Returns
        -------
        bool
            True if the point is inside the cylinder.
            False otherwise.

        """
        point = self.frame.to_local_coordinates(point)
        x, y, z = point.x, point.y, point.z  # type: ignore

        if z > self.height / 2 + tol:
            return False
        if z < -self.height / 2 - tol:
            return False

        return x**2 + y**2 <= (self.radius + tol) ** 2


# src/compas/geometry/shapes/shape.py

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

from compas.geometry import Geometry
from compas.geometry import Frame
from compas.geometry import Transformation
from compas.geometry import Rotation
from compas.geometry import Plane


class Shape(Geometry):
    """Base class for geometric shapes.

    Shapes are parametrically defined with repsect to a local coordinate system.
    The local coordinate system of a shape is defined by a frame.
    The default frame is the word coordinate system, i.e. the origin and the axes of the world XY plane.

    Shapes are finite, closed objects, with a boundary that separates the interior from the exterior.
    They have a well-defined surface area and volume.

    An explicit representation of a shape is obtained by discretising its boundary into a set of vertices and faces with a chosen resolution (:meth:`to_vertices_and_faces`).
    The vertices and faces can be used to construct a :class:`compas.geometry.Polyhedron` object (:meth:`to_polyhedron`).
    A shape can also be converted to a :class:`compas.geometry.Brep` object (:meth:`to_brep`).

    Breps and polyhedrons support boolean operations.

    Parameters
    ----------
    frame : :class:`compas.geometry.Frame`, optional
        The local coordinate system of the shape.
        Default is ``None``, in which case the world coordinate system is used.

    Attributes
    ----------
    frame : :class:`compas.geometry.Frame`
        The local coordinate system of the shape.
    transformation : :class:`compas.geometry.Transformation`, read-only
        The transformation of the shape to global coordinates.
    normal : :class:`compas.geometry.Vector`, read-only
        The normal of the shape.
    plane : :class:`compas.geometry.Plane`, read-only
        The plane of the shape.
    area : float, read-only
        The surface area of the shape.
    volume : float, read-only
        The volume of the shape.

    """

    def __init__(self, frame=None, **kwargs):
        super(Shape, self).__init__(**kwargs)
        self._frame = None
        self._transformation = None
        self.frame = frame

    # =============================================================================
    # Data
    # =============================================================================

    @property
    def data(self):
        raise NotImplementedError

    @data.setter
    def data(self, data):
        raise NotImplementedError

    @classmethod
    def from_data(cls, data):
        """Construct a shape from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Shape`
            The constructed shape.

        """
        return cls(**data)

    # =============================================================================
    # Properties
    # =============================================================================

    @property
    def frame(self):
        if not self._frame:
            self._frame = Frame.worldXY()
        return self._frame

    @frame.setter
    def frame(self, frame):
        if not frame:
            self._frame = None
        else:
            self._frame = Frame(frame[0], frame[1], frame[2])
        self._transformation = None

    @property
    def transformation(self):
        if not self._transformation:
            self._transformation = Transformation.from_frame(self.frame)
        return self._transformation

    @property
    def point(self):
        return self.frame.point

    @point.setter
    def point(self, point):
        self.frame.point = point

    @property
    def normal(self):
        return self.frame.zaxis

    @property
    def plane(self):
        return Plane(self.frame.point, self.frame.zaxis)

    @property
    def area(self):
        raise NotImplementedError

    @property
    def volume(self):
        raise NotImplementedError

    # =============================================================================
    # Constructors
    # =============================================================================

    # =============================================================================
    # Conversions
    # =============================================================================

    def to_vertices_and_faces(self, **kwargs):
        """Convert the shape to a list of vertices and faces.

        Returns
        -------
        list of list of float
            The vertices of the shape.
        list of list of int
            The faces of the shape.

        """
        raise NotImplementedError

    def to_polyhedron(self, triangulated=True, u=16, v=None):
        """Convert the shape to a polyhedron.

        Parameters
        ----------
        triangulated : bool, optional
            If True, triangulate the faces.
        u : int, optional
            Number of faces in the "u" direction.
        v : int, optional
            Number of faces in the "v" direction.
            If no value is provided, and the shape has two parameter directions, the value of ``u`` will be used.

        Returns
        -------
        :class:`compas.geometry.Polyhedron`
            The polyhedron representation of the shape.

        Notes
        -----
        Parameters ``u`` and ``v`` define the resolution of the discretisation of curved geometry.
        If the geometry is not curved in a particular direction, the corresponding parameter will be ignored.
        For example, a cylinder has a resolution in the "u" direction, but not in the "v" direction.
        A sphere has a resolution in both the "u" and the "v" direction.
        A box has no resolution in either direction.

        """
        from compas.geometry import Polyhedron

        v = v or u

        vertices, faces = self.to_vertices_and_faces(u=u, v=v)

        if triangulated:
            triangles = []
            for face in faces:
                if len(face) == 4:
                    triangles.append(face[0:3])
                    triangles.append([face[0], face[2], face[3]])
                else:
                    triangles.append(face)
            faces = triangles

        return Polyhedron(vertices, faces)

    def to_brep(self):
        """Convert the shape to a Brep.

        Returns
        -------
        :class:`compas.geometry.Brep`
            The Brep representation of the shape.

        """
        raise NotImplementedError

    # =============================================================================
    # Transformation
    # =============================================================================

    def transform(self, transformation):
        """Transform the shape.

        Transformations of a shape are performed by applying the transformation to the frame of the shape.
        Transformations of the shape with respect to its local coordinate system are not supported.
        For this reason, only (combinations of) translations and rotations are supported.
        To scale a shape, use the :meth:`Shape.scale` method.

        Parameters
        ----------
        transformation : :class:`Transformation`
            The transformation used to transform the shape.

        Returns
        -------
        None

        """
        self.frame.transform(transformation)

    def translate(self, vector):
        """Translate the shape.

        Parameters
        ----------
        vector : :class:`Vector`
            The translation vector.

        Returns
        -------
        None

        """
        self.frame.point += vector

    def rotate(self, angle, axis=None, point=None):
        """Rotate the shape.

        Parameters
        ----------
        vector : :class:`Vector`
            The translation vector.

        Returns
        -------
        None

        """
        point = point or [0, 0, 0]
        matrix = Rotation.from_axis_and_angle(axis=axis, angle=angle, point=point)
        self.transform(matrix)

    def scale(self, scale):
        """Scale the shape.

        Scale transformations are applied to the parameters of a shape rahter than to its frame.
        Only uniform scaling is supported.

        Parameters
        ----------
        scale : float
            The scaling factor.

        Returns
        -------
        None

        """
        raise NotImplementedError

    # =============================================================================
    # Methods
    # =============================================================================

    def contains_point(self, point):
        """Verify if a point is inside the shape.

        Parameters
        ----------
        point : :class:`Point`
            The point to test.

        Returns
        -------
        bool
            True if the point is inside the shape.
            False otherwise.

        """
        raise NotImplementedError

    def contains_points(self, points):
        """Verify if a list of points are inside the shape.

        Parameters
        ----------
        points : list of :class:`Point`
            The points to test.

        Returns
        -------
        list of bool
            True if the point is inside the shape.
            False otherwise.

        """
        return [self.contains_point(point) for point in points]


# src/compas/geometry/shapes/sphere.py

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

from math import cos
from math import pi
from math import sin

from compas.geometry import transform_points
from compas.geometry import Frame
from compas.geometry import Line
from compas.geometry import Circle
from .shape import Shape


class Sphere(Shape):
    """A sphere is defined by a point and a radius.

    Parameters
    ----------
    frame: :class:`~compas.geometry.Frame`, optional
        The local coordinates system, or "frame", of the sphere.
        Default is ``None``, in which case the sphere is constructed in world coordinates.
    radius: float, optional
        The radius of the sphere.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The coordinate system of the sphere.
    transformation : :class:`~compas.geometry.Transformation`
        The transformation of the sphere to global coordinates.
    radius : float
        The radius of the sphere.
    axis : :class:`~compas.geometry.Line`, read-only
        The central axis of the sphere.
    base : :class:`~compas.geometry.Point`, read-only
        The base point of the sphere.
        The base point is at the origin of the local coordinate system.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the sphere.
        The base point of the plane is at the origin of the local coordinate system.
        The normal of the plane is in the direction of the z-axis of the local coordinate system.
    circle : :class:`~compas.geometry.Circle`, read-only
        The base circle of the sphere.
        The center of the circle is at the origin of the local coordinate system.
    diameter : float, read-only
        The diameter of the sphere.
    area : float, read-only
        The surface area of the sphere.
    volume : float, read-only
        The volume of the sphere.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> from compas.geometry import Sphere
    >>> sphere1 = Sphere(frame=Frame.worldXY(), radius=5)
    >>> sphere1 = Sphere(radius=5)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "radius": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "radius"],
    }

    def __init__(self, frame=None, radius=None, point=None, **kwargs):
        super(Sphere, self).__init__(frame=frame, **kwargs)
        self._radius = 1.0
        self.radius = radius
        if point:
            self.frame.point = point

    def __repr__(self):
        return "Sphere(frame={0!r}, radius={1!r})".format(self.frame, self.radius)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.radius
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.radius = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.radius])

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"frame": self.frame, "radius": self.radius}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.radius = data["radius"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("The radius of the sphere is not set.")
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("The radius of the sphere should be larger than or equal to zero.")
        self._radius = float(radius)

    @property
    def start(self):
        return self.frame.point + self.frame.zaxis * -self.radius

    @property
    def end(self):
        return self.frame.point + self.frame.zaxis * +self.radius

    @property
    def axis(self):
        return Line(self.start, self.end)

    @property
    def circle(self):
        return Circle(self.frame, self.radius)

    @property
    def area(self):
        return 4 * pi * self.radius**2

    @property
    def volume(self):
        return 4.0 / 3.0 * pi * self.radius**3

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_point_and_radius(cls, point, radius):
        """Construct a sphere from a point and a radius.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The center of the sphere.
        radius : float
            The radius of the sphere.

        Returns
        -------
        :class:`~compas.geometry.Sphere`
            The constructed sphere.

        """
        frame = Frame.worldXY()
        frame.point = point
        return cls(frame=frame, radius=radius)

    # ==========================================================================
    # Conversions
    # ==========================================================================

    def to_vertices_and_faces(self, u=16, v=16, triangulated=False):
        """Returns a list of vertices and faces

        The vertex positions are in world coordinates.

        Parameters
        ----------
        u : int, optional
            Number of faces in the "u" direction.
        v : int, optional
            Number of faces in the "v" direction.
        triangulated: bool, optional
            If True, triangulate the faces.

        Returns
        -------
        list[list[float]]
            A list of vertex locations.
        list[list[int]]
            And a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if u < 3:
            raise ValueError("The value for u should be u > 3.")
        if v < 3:
            raise ValueError("The value for v should be v > 3.")

        theta = pi / v
        phi = pi * 2 / u
        hpi = pi * 0.5

        x, y, z = self.frame.point

        vertices = []
        for i in range(1, v):
            for j in range(u):
                tx = self.radius * cos(i * theta - hpi) * cos(j * phi) + x
                ty = self.radius * cos(i * theta - hpi) * sin(j * phi) + y
                tz = self.radius * sin(i * theta - hpi) + z
                vertices.append([tx, ty, tz])

        vertices.append([x, y, z + self.radius])
        vertices.append([x, y, z - self.radius])

        faces = []

        # south pole triangle fan
        sp = len(vertices) - 1
        for j in range(u):
            faces.append([sp, (j + 1) % u, j])

        for i in range(v - 2):
            for j in range(u):
                jj = (j + 1) % u
                a = i * u + j
                b = i * u + jj
                c = (i + 1) * u + jj
                d = (i + 1) * u + j
                faces.append([a, b, c, d])

        # north pole triangle fan
        np = len(vertices) - 2
        for j in range(u):
            nc = len(vertices) - 3 - j
            nn = len(vertices) - 3 - (j + 1) % u
            faces.append([np, nn, nc])

        if triangulated:
            triangles = []
            for face in faces:
                if len(face) == 4:
                    triangles.append(face[0:3])
                    triangles.append([face[0], face[2], face[3]])
                else:
                    triangles.append(face)
            faces = triangles

        # transform the vertices to world coordinates
        vertices = transform_points(vertices, self.transformation)

        return vertices, faces

    # =============================================================================
    # Transformations
    # =============================================================================

    # def transform(self, transformation):
    #     """Transform the sphere.

    #     Parameters
    #     ----------
    #     transformation : :class:`~compas.geometry.Transformation`
    #         The transformation used to transform the Sphere.
    #         Note that non-similarity preserving transformations will not change
    #         the sphere into an ellipsoid. In such case, the radius of the sphere
    #         will be scaled by the largest scale factor of the threee axis.

    #     Returns
    #     -------
    #     None

    #     Examples
    #     --------
    #     >>> from compas.geometry import Frame
    #     >>> from compas.geometry import Transformation
    #     >>> from compas.geometry import Sphere
    #     >>> sphere = Sphere(Point(1, 1, 1), 5)
    #     >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    #     >>> T = Transformation.from_frame(frame)
    #     >>> sphere.transform(T)

    #     """
    #     self.frame.transform(transformation)
    #     Sc, _, _, _, _ = transformation.decomposed()
    #     self.radius *= max([Sc[0, 0], Sc[1, 1], Sc[2, 2]])

    def scale(self, factor):
        """Scale the sphere.

        Parameters
        ----------
        factor : float
            The scaling factor.

        Returns
        -------
        None

        """
        self.radius *= factor

    # =============================================================================
    # Methods
    # =============================================================================

    def contains_point(self, point, tol=1e-6):
        """Verify if a point is inside the sphere.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The point to test.
        tol : float, optional
            The tolerance for the test.

        Returns
        -------
        bool
            True if the point is inside the sphere.
            False otherwise.

        """
        return self.frame.point.distance_to_point(point) <= self.radius + tol

    def contains_points(self, points, tol=1e-6):
        """Verify if a list of points are inside the sphere.

        Parameters
        ----------
        points : list of :class:`~compas.geometry.Point`
            The points to test.
        tol : float, optional
            The tolerance for the test.

        Returns
        -------
        list
            A list of booleans indicating for each point if it is inside the sphere.

        """
        return [self.contains_point(point, tol=tol) for point in points]


# src/compas/geometry/shapes/torus.py

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

from math import cos
from math import pi
from math import sin

from compas.geometry import matrix_from_frame
from compas.geometry import transform_points
from compas.geometry import Frame
from compas.geometry import Plane

from .shape import Shape


class Torus(Shape):
    """A torus is defined by a plane and two radii.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`, optional
        The local coordinate system of the torus.
        Default is ``None``, in which case the torus is constructed in the
    radius_axis: float, optional
        The radius of the axis.
    radius_pipe: float, optional
        The radius of the pipe.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The coordinate system of the torus.
    transformation : :class:`~compas.geometry.Transformation`
        The transformation of the sphere to global coordinates.
    radius_axis : float
        The radius of the axis.
    radius_pipe : float
        The radius of the pipe.
    axis : :class:`~compas.geometry.Line`, read-only
        The central axis of the torus.
    base : :class:`~compas.geometry.Point`, read-only
        The base point of the torus.
        The base point is at the origin of the local coordinate system.
    plane : :class:`~compas.geometry.Plane`, read-only
        The plane of the torus.
        The base point of the plane is at the origin of the local coordinate system.
        The normal of the plane is in the direction of the z-axis of the local coordinate system.
    circle : :class:`~compas.geometry.Circle`, read-only
        The base circle of the torus.
        The center of the circle is at the origin of the local coordinate system.
    area : float, read-only
        The surface area of the torus.
    volume : float, read-only
        The volume of the torus.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> from compas.geometry import Torus
    >>> torus = Torus(frame=Frame.worldXY(), radius_axis=5.0, radius_pipe=2.0)
    >>> torus = Torus(radius_axis=5.0, radius_pipe=2.0)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "frame": Frame.JSONSCHEMA,
            "radius_axis": {"type": "number", "minimum": 0},
            "radius_pipe": {"type": "number", "minimum": 0},
        },
        "required": ["frame", "radius_axis", "radius_pipe"],
    }

    def __init__(self, frame=None, radius_axis=1.0, radius_pipe=0.3, **kwargs):
        super(Torus, self).__init__(frame=frame, **kwargs)
        self._radius_axis = None
        self._radius_pipe = None
        self.radius_axis = radius_axis
        self.radius_pipe = radius_pipe

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        return {
            "frame": self.frame,
            "radius_axis": self.radius_axis,
            "radius_pipe": self.radius_pipe,
        }

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.radius_axis = data["radius_axis"]
        self.radius_pipe = data["radius_pipe"]

    @classmethod
    def from_data(cls, data):
        """Construct a torus from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Torus`
            The constructed torus.

        Examples
        --------
        >>> data = {"frame": Frame.worldXY(), "radius_axis": 1.0, "radius_pipe": 0.3}
        >>> torus = Torus.from_data(data)

        """
        return cls(**data)

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def plane(self):
        return Plane(self.frame.point, self.frame.zaxis)

    @property
    def radius_axis(self):
        if self._radius_axis is None:
            raise ValueError("The torus has no axis radius.")
        return self._radius_axis

    @radius_axis.setter
    def radius_axis(self, radius):
        if radius < 0:
            raise ValueError("The value for the axis radius should be radius >= 0.")
        self._radius_axis = float(radius)

    @property
    def radius_pipe(self):
        if self._radius_pipe is None:
            raise ValueError("The torus has no pipe radius.")
        return self._radius_pipe

    @radius_pipe.setter
    def radius_pipe(self, radius):
        if radius < 0:
            raise ValueError("The value for the pipe radius should be radius >= 0.")
        self._radius_pipe = float(radius)

    @property
    def center(self):
        return self.plane.point

    @property
    def area(self):
        return (2 * pi * self.radius_pipe) * (2 * pi * self.radius_axis)

    @property
    def volume(self):
        return (pi * self.radius_pipe**2) * (2 * pi * self.radius_axis)

    # ==========================================================================
    # customisation
    # ==========================================================================

    def __repr__(self):
        return "Torus(frame={0!r}, radius_axis={1!r}, radius_pipe={2!r})".format(
            self.frame, self.radius_axis, self.radius_pipe
        )

    def __len__(self):
        return 3

    def __getitem__(self, key):
        if key == 0:
            return self.frame
        elif key == 1:
            return self.radius_axis
        elif key == 2:
            return self.radius_pipe
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.frame = value
        elif key == 1:
            self.radius_axis = value
        elif key == 2:
            self.radius_pipe = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.frame, self.radius_axis, self.radius_pipe])

    # ==========================================================================
    # constructors
    # ==========================================================================

    @classmethod
    def from_plane_and_radii(cls, plane, radius_axis, radius_pipe):
        """Construct a torus from a plane and two radii.

        Parameters
        ----------
        plane : :class:`~compas.geometry.Plane`
            The plane of the torus.
        radius_axis: float
            The radius of the axis.
        radius_pipe: float
            The radius of the pipe.

        Returns
        -------
        :class:`~compas.geometry.Torus`
            The constructed torus.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> from compas.geometry import Torus
        >>> plane = Plane.worldXY()
        >>> torus = Torus.from_plane_and_radii(plane, 5.0, 2.0)

        """
        return cls(plane=plane, radius_axis=radius_axis, radius_pipe=radius_pipe)

    # ==========================================================================
    # methods
    # ==========================================================================

    def to_vertices_and_faces(self, u=16, v=16, triangulated=False):
        """Returns a list of vertices and faces.

        The vertices are defined in global coordinates.

        Parameters
        ----------
        u : int, optional
            Number of faces in the "u" direction.
        v : int, optional
            Number of faces in the "v" direction.
        triangulated: bool, optional
            If True, triangulate the faces.

        Returns
        -------
        list[list[float]], list[list[int]]
            A list of vertex locations, and a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if u < 3:
            raise ValueError("The value for u should be u > 3.")
        if v < 3:
            raise ValueError("The value for v should be v > 3.")

        theta = pi * 2 / u
        phi = pi * 2 / v
        vertices = []
        for i in range(u):
            for j in range(v):
                x = cos(i * theta) * (self.radius_axis + self.radius_pipe * cos(j * phi))
                y = sin(i * theta) * (self.radius_axis + self.radius_pipe * cos(j * phi))
                z = self.radius_pipe * sin(j * phi)
                vertices.append([x, y, z])

        # transform vertices to torus' plane
        frame = Frame.from_plane(self.plane)
        M = matrix_from_frame(frame)
        vertices = transform_points(vertices, M)

        faces = []
        for i in range(u):
            ii = (i + 1) % u
            for j in range(v):
                jj = (j + 1) % v
                a = i * v + j
                b = ii * v + j
                c = ii * v + jj
                d = i * v + jj
                faces.append([a, b, c, d])

        if triangulated:
            triangles = []
            for face in faces:
                if len(face) == 4:
                    triangles.append(face[0:3])
                    triangles.append([face[0], face[2], face[3]])
                else:
                    triangles.append(face)
            faces = triangles

        vertices = transform_points(vertices, self.transformation)

        return vertices, faces

    def transform(self, transformation):
        """Transform the torus.

        Parameters
        ----------
        transformation : :class:`~compas.geometry.Transformation`
            The transformation used to transform the Torus.

        Returns
        -------
        None

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> from compas.geometry import Transformation
        >>> from compas.geometry import Torus
        >>> torus = Torus(Frame.worldXY(), 5, 2)
        >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(frame)
        >>> torus.transform(T)

        """
        self.frame.transform(transformation)


# src/compas/geometry/triangulation/__init__.py

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

from compas.plugins import pluggable


@pluggable(category="triangulation")
def delaunay_triangulation(points):
    """Construct a Delaunay triangulation of set of vertices.

    Parameters
    ----------
    points : list
        XY(Z) coordinates of the points to triangulate.

    Returns
    -------
    (list, list)
        The vertices of the triangulation, and the faces of the triangulation.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


@pluggable(category="triangulation")
def constrained_delaunay_triangulation(boundary, polylines=None, polygons=None):
    """Construct a Delaunay triangulation of set of vertices, constrained to the specified segments.

    Parameters
    ----------
    boundary : list
        Ordered points on the boundary.
    polylines : list, optional
        Lists of ordered points defining internal guide curves.
    polygons : list, optional
        Lists of ordered points defining holes in the triangulation.

    Returns
    -------
    (list, list)
        The vertices of the triangulation, and the faces of the triangulation.

    Notes
    -----
    No additional points will be inserted in the triangulation.

    Examples
    --------
    >>>

    """
    pass


@pluggable(category="triangulation")
def conforming_delaunay_triangulation(boundary, polylines=None, polygons=None, angle=None, area=None):
    """Construct a Conforming Delaunay triangulation of set of vertices, constrained to the specified segments.

    Parameters
    ----------
    boundary : list
        Ordered points on the boundary.
    polylines : list, optional
        Lists of ordered points defining internal guide curves.
    polygons : list, optional
        Lists of ordered points defining holes in the triangulation.
    angle : float, optional
        Minimum angle constraint for the triangles of the triangulation.
        If an angle constraint is given, "Steiner points" may be inserted internally
        and along the constraint segments to satisfy the constraint.
        The angle constraint should be specified in degrees.
    area : float, optional
        Maximum area constraint for the triangles of the triangulation.
        If an area constraint is given, "Steiner points" may be inserted internally
        and along the constraint segments to satisfy the constraint.

    Returns
    -------
    (list, list)
        The vertices of the triangulation, and the faces of the triangulation.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/triangulation/delaunay.py

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

import random

from compas.geometry import centroid_points
from compas.geometry import distance_point_point
from compas.geometry import add_vectors
from compas.geometry import bounding_box

from compas.geometry import is_point_in_polygon_xy
from compas.geometry import is_point_in_triangle_xy
from compas.geometry import is_point_in_circle_xy

from compas.geometry import circle_from_points_xy


def delaunay_from_points(points, boundary=None, holes=None, tiny=1e-12):
    """Computes the delaunay triangulation for a list of points.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        XYZ coordinates of the original points.
    boundary : sequence[[float, float, float] | :class:`~compas.geometry.Point`] | :class:`~compas.geometry.Polygon`, optional
        List of ordered points describing the outer boundary.
    holes : sequence[sequence[[float, float, float] | :class:`~compas.geometry.Point`] | :class:`~compas.geometry.Polygon`], optional
        List of polygons (ordered points describing internal holes.

    Returns
    -------
    list[[int, int, int]]
        The faces of the triangulation.
        Each face is a triplet of indices referring to the list of point coordinates.

    Notes
    -----
    For more info, see [1]_.

    References
    ----------
    .. [1] Sloan, S. W., 1987 *A fast algorithm for constructing Delaunay triangulations in the plane*
           Advances in Engineering Software 9(1): 34-55, 1978.

    Examples
    --------
    >>>

    """
    from compas.datastructures import Mesh
    from compas.datastructures import trimesh_swap_edge

    def super_triangle(coords, ccw=True):
        centpt = centroid_points(coords)
        bbpts = bounding_box(coords)
        dis = distance_point_point(bbpts[0], bbpts[2])
        dis = dis * 300
        v1 = (0 * dis, 2 * dis, 0)
        v2 = (1.73205 * dis, -1.0000000000001 * dis, 0)  # due to numerical issues
        v3 = (-1.73205 * dis, -1 * dis, 0)
        pt1 = add_vectors(centpt, v1)
        pt2 = add_vectors(centpt, v2)
        pt3 = add_vectors(centpt, v3)
        if ccw:
            return pt1, pt3, pt2
        return pt1, pt2, pt3

    mesh = Mesh()

    # to avoid numerical issues for perfectly structured point sets
    points = [
        (
            point[0] + random.uniform(-tiny, tiny),
            point[1] + random.uniform(-tiny, tiny),
            0.0,
        )
        for point in points
    ]

    # create super triangle
    pt1, pt2, pt3 = super_triangle(points)

    # add super triangle vertices to mesh
    n = len(points)
    super_keys = n, n + 1, n + 2

    mesh.add_vertex(super_keys[0], {"x": pt1[0], "y": pt1[1], "z": pt1[2]})
    mesh.add_vertex(super_keys[1], {"x": pt2[0], "y": pt2[1], "z": pt2[2]})
    mesh.add_vertex(super_keys[2], {"x": pt3[0], "y": pt3[1], "z": pt3[2]})

    mesh.add_face(super_keys)

    # iterate over points
    for key, point in enumerate(points):
        # newtris should be intialised here

        # check in which triangle this point falls
        for fkey in list(mesh.faces()):
            abc = mesh.face_coordinates(fkey)

            if is_point_in_triangle_xy(point, abc, True):
                # generate 3 new triangles (faces) and delete surrounding triangle
                key, newtris = mesh.insert_vertex(fkey, key=key, xyz=point, return_fkeys=True)
                break

        while newtris:
            fkey = newtris.pop()

            face = mesh.face_vertices(fkey)
            i = face.index(key)
            u = face[i - 2]
            v = face[i - 1]

            nbr = mesh.halfedge[v][u]

            if nbr is not None:
                a, b, c = mesh.face_coordinates(nbr)
                circle = circle_from_points_xy(a, b, c)

                if is_point_in_circle_xy(point, circle):
                    fkey, nbr = trimesh_swap_edge(mesh, (u, v))
                    newtris.append(fkey)
                    newtris.append(nbr)

    # Delete faces adjacent to supertriangle
    for key in super_keys:
        mesh.delete_vertex(key)

    # Delete faces outside of boundary
    if boundary:
        for fkey in list(mesh.faces()):
            centroid = mesh.face_centroid(fkey)
            if not is_point_in_polygon_xy(centroid, boundary):
                mesh.delete_face(fkey)

    # Delete faces inside of inside boundaries
    if holes:
        for polygon in holes:
            for fkey in list(mesh.faces()):
                centroid = mesh.face_centroid(fkey)
                if is_point_in_polygon_xy(centroid, polygon):
                    mesh.delete_face(fkey)

    return [mesh.face_vertices(fkey) for fkey in mesh.faces()]


# def voronoi_from_delaunay(delaunay):
#     """Construct the Voronoi dual of the triangulation of a set of points.

#     Parameters
#     ----------
#     delaunay : Mesh
#         A delaunay mesh.

#     Returns
#     -------
#     Mesh
#         The corresponding voronoi mesh.

#     Warnings
#     --------
#     This function does not work properly if all vertices of the delaunay
#     are on the boundary.

#     Examples
#     --------
#     .. plot::
#         :include-source:

#         from compas.datastructures import Mesh
#         from compas.datastructures import trimesh_remesh
#         from compas.geometry import delaunay_from_points
#         from compas.geometry import voronoi_from_delaunay
#         from compas.geometry import pointcloud_xy
#         from compas_plotters import MeshPlotter

#         points = pointcloud_xy(10, (0, 10))
#         faces = delaunay_from_points(points)
#         delaunay = Mesh.from_vertices_and_faces(points, faces)

#         trimesh_remesh(delaunay, 1.0, allow_boundary_split=True)

#         points = [delaunay.vertex_coordinates(key) for key in delaunay.vertices()]
#         faces = delaunay_from_points(points)
#         delaunay = Mesh.from_vertices_and_faces(points, faces)

#         voronoi = voronoi_from_delaunay(delaunay)

#         lines = []
#         for u, v in voronoi.edges():
#             lines.append({
#                 'start': voronoi.vertex_coordinates(u, 'xy'),
#                 'end'  : voronoi.vertex_coordinates(v, 'xy'),
#                 'width': 1.0
#             })

#         plotter = MeshPlotter(delaunay)

#         plotter.draw_lines(lines)

#         plotter.draw_vertices(
#             radius=0.075,
#             facecolor={key: '#0092d2' for key in delaunay.vertices() if key not in delaunay.vertices_on_boundary()})

#         plotter.draw_edges(color='#cccccc')

#         plotter.show()

#     """
#     voronoi = mesh_dual(delaunay)

#     for key in voronoi.vertices():
#         a, b, c = delaunay.face_coordinates(key)
#         center, radius, normal = circle_from_points_xy(a, b, c)
#         voronoi.vertex[key]['x'] = center[0]
#         voronoi.vertex[key]['y'] = center[1]
#         voronoi.vertex[key]['z'] = center[2]

#     return voronoi


# src/compas/geometry/triangulation/delaunay_numpy.py

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

from numpy import asarray
from scipy.spatial import Voronoi
from scipy.spatial import Delaunay


def delaunay_from_points_numpy(points):
    """Computes the delaunay triangulation for a list of points using Numpy.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        XYZ coordinates of the original points.

    Returns
    -------
    list[[int, int, int]]
        The faces of the triangulation.
        Each face is a triplet of indices referring to the list of point coordinates.

    Examples
    --------
    >>>

    """
    xyz = asarray(points)
    d = Delaunay(xyz[:, 0:2])
    return d.simplices


def voronoi_from_points_numpy(points):
    """Generate a voronoi diagram from a set of points.

    Parameters
    ----------
    points : sequence[[float, float, float] | :class:`~compas.geometry.Point`]
        XYZ coordinates of the voronoi sites.

    Returns
    -------

    Examples
    --------
    >>>

    """
    points = asarray(points)
    voronoi = Voronoi(points)
    return voronoi


# src/compas/geometry/triangulation/earclip.py

from compas.geometry import is_ccw_xy, is_point_in_triangle_xy


def earclip_polygon(polygon):
    """Triangulate a polygon using the ear clipping method.

    Parameters
    ----------
    polygon : :class:`compas.geometry.Polygon` | list[:class:`compas.geometry.Point`]
        A polygon defined by a sequence of points.

    Returns
    -------
    list[[point, point, point]]
        A list of triangles.

    Raises
    ------
    Exception
        If not all points were consumed by the procedure.

    """

    def find_ear(points, point_index):
        p = len(points)
        if p == 3:
            triangle = [
                point_index[id(points[0])],
                point_index[id(points[1])],
                point_index[id(points[2])],
            ]
            del points[2]
            del points[1]
            del points[0]
            return triangle
        for i in range(-2, p - 2):
            a = points[i]
            b = points[i + 1]
            c = points[i + 2]
            is_valid = True
            if not is_ccw_xy(b, c, a):
                continue
            for j in range(p):
                if j == i or j == i + 1 or j == i + 2:
                    continue
                if is_point_in_triangle_xy(points[j], (a, b, c)):
                    is_valid = False
                    break
            if is_valid:
                del points[i + 1]
                return [point_index[id(a)], point_index[id(b)], point_index[id(c)]]

    points = list(polygon)
    point_index = {id(point): index for index, point in enumerate(points)}

    triangles = []
    while len(points) >= 3:
        ear = find_ear(points, point_index)
        triangles.append(ear)

    if points:
        raise Exception("Not all points were consumed by the clipping procedure.")

    return triangles


# src/compas/geometry/trimesh/curvature.py

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

from compas.plugins import pluggable


@pluggable(category="trimesh")
def trimesh_gaussian_curvature(M):
    """Compute the discrete gaussian curvature of a triangle mesh.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :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.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


@pluggable(category="trimesh")
def trimesh_principal_curvature(M):
    """Compute the principal curvature directions of a triangle mesh.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.

    Returns
    -------
    list[tuple[[float, float, float], [float, float]]]
        The curvature directions per vertex.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


@pluggable(category="trimesh")
def trimesh_mean_curvature(M):
    """Compute the discrete mean curvature of a triangle mesh.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :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.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/trimesh/geodistance.py

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

from compas.plugins import pluggable


@pluggable(category="trimesh")
def trimesh_geodistance(M, source, method="exact"):
    """Compute the geodesic distance from every vertex of the mesh to a source vertex.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.
    source : int
        The index of the vertex from where the geodesic distances should be calculated.
    method : Literal['exact', 'heat'], optional
        The method for calculating the distances.

    Returns
    -------
    list[float]
        A list of geodesic distances from the source vertex.

    Raises
    ------
    NotImplementedError
        If `method` is not one of ``{'exact', 'heat'}``.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/trimesh/isolines.py

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

from compas.plugins import pluggable


@pluggable(category="trimesh")
def trimesh_isolines(M, S, N=50):
    """Compute isolines on a triangle mesh using a scalarfield of data points
    assigned to its vertices.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.
    S : list[float]
        A list of scalars.
    N : int, optional
        The number of isolines.

    Returns
    -------
    list[[float, float, float]]
        The coordinates of the polyline points.
    list[[int, int]]
        The segments of the polylines defined as pairs of points.

    Notes
    -----
    To convert the vertices and edges to sets of isolines, use :func:`groupsort_isolines`

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/trimesh/matrices.py

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

from compas.plugins import pluggable


@pluggable(category="trimesh")
def trimesh_massmatrix(M):
    """Compute massmatrix on a triangle mesh using a scalarfield of data points
    assigned to its vertices.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :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 mass per vertex.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/trimesh/parametrisation.py

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

from compas.plugins import pluggable


@pluggable(category="trimesh")
def trimesh_harmonic(M):
    """Compute the harmonic parametrisation of a triangle mesh within a fixed circular boundary.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.

    Returns
    -------
    list[[int, int]]
        The u, v parameters per vertex.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


@pluggable(category="trimesh")
def trimesh_lscm(M):
    """Compute the least squares conformal map of a triangle mesh.

    Parameters
    ----------
    M : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.

    Returns
    -------
    list[[int, int]]
        The u, v parameters per vertex.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/trimesh/remesh.py

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

from compas.plugins import pluggable


@pluggable(category="trimesh")
def trimesh_remesh(mesh, target_edge_length, number_of_iterations=10, do_project=True):
    """Remeshing of a triangle mesh.

    Parameters
    ----------
    mesh : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.
    target_edge_length : float
        The target edge length.
    number_of_iterations : int, optional
        Number of remeshing iterations.
    do_project : bool, optional
        Reproject vertices onto the input surface when they are created or displaced.

    Returns
    -------
    list[[float, float, float]]
        Vertices of the remeshed mesh.
    list[[int, int, int]]
        Faces of the remeshed mesh.

    Notes
    -----
    This remeshing function only constrains the edges on the boundary of the mesh.
    To protect specific features or edges, please use :func:`remesh_constrained`.

    """
    raise NotImplementedError


@pluggable(category="trimesh")
def trimesh_remesh_constrained(mesh, target_edge_length, protected_edges, number_of_iterations=10, do_project=True):
    """Constrained remeshing of a triangle mesh.

    Parameters
    ----------
    mesh : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.
    target_edge_length : float
        The target edge length.
    protected_edges : list[[int, int]]
        A list of vertex pairs that identify protected edges of the mesh.
    number_of_iterations : int, optional
        Number of remeshing iterations.
    do_project : bool, optional
        Reproject vertices onto the input surface when they are created or displaced.

    Returns
    -------
    list[[float, float, float]]
        Vertices of the remeshed mesh.
    list[[int, int, int]]
        Faces of the remeshed mesh.

    """
    raise NotImplementedError


@pluggable(category="trimesh")
def trimesh_remesh_along_isoline(mesh, scalarfield, scalar):
    """Remesh a mesh along an isoline of a scalarfield over the vertices.

    Parameters
    ----------
    mesh : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.
    scalarfield : sequence[float]
        A scalar value per vertex of the mesh.
    scalar : float
        A value within the range of the scalarfield.

    Returns
    -------
    list[[float, float, float]]
        Vertices of the remeshed mesh.
    list[[int, int, int]]
        Faces of the remeshed mesh.

    Examples
    --------
    >>>

    """
    raise NotImplementedError


# src/compas/geometry/trimesh/slicing.py

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

from compas.plugins import pluggable


@pluggable(category="trimesh")
def trimesh_slice(mesh, planes):
    """Slice a mesh by a list of planes.

    Parameters
    ----------
    mesh : tuple[sequence[[float, float, float] | :class:`~compas.geometry.Point`], sequence[[int, int, int]]]
        A mesh represented by a list of vertices and a list of faces.
    planes : sequence[[point, vector] | :class:`~compas.geometry.Plane`]
        The slicing planes.

    Returns
    -------
    list[list[float, float, float]]
        The points defining the slice polylines.

    """
    raise NotImplementedError


# src/compas/__init__.py

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,
)
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-2022 - ETH Zurich, Copyright 2023 - COMPAS Association"
__license__ = "MIT License"
__email__ = "tom.v.mele@gmail.com"
__version__ = "1.17.5"

version = LooseVersion(compas.__version__)
versionstring = version.vstring.split("-")[0]

HERE = compas._os.realpath(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 installation."""

DATA = compas._os.absjoin(HERE, "data", "samples")
"""str: Path to the data folder of the local installation."""

TEMP = compas._os.absjoin(HERE, "../../temp")
"""str: Path to the temp folder of the local installation."""

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.
"""

PY2 = compas._os.PY2
"""bool: True if the current Python version is 2.x, False otherwise."""

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",
    "PY2",
    "PY3",
    "is_windows",
    "is_linux",
    "is_osx",
    "is_mono",
    "is_ironpython",
    "is_rhino",
    "is_blender",
    "is_grasshopper",
    "set_precision",
    "get",
    "json_dump",
    "json_load",
    "json_dumps",
    "json_loads",
]

__all_plugins__ = [
    "compas.geometry.booleans.booleans_shapely",
]


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.

    Returns
    -------
    str
        The full path to the specified file.
        The path can be local or remote depending on availability.

    Notes
    -----
    The file name should be specified relative to the **COMPAS** sample data folder.
    If the requested file is not locally available in the sample data folder,
    the function will get the corresponding file path from
    the GitHub repo, at https://raw.githubusercontent.com/compas-dev/compas/main/samples

    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://raw.githubusercontent.com/compas-dev/compas/main/src/compas/data/samples/{}".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:

            def is_within_directory(directory, target):
                abs_directory = os.path.abspath(directory)
                abs_target = os.path.abspath(target)

                prefix = os.path.commonprefix([abs_directory, abs_target])

                return prefix == abs_directory

            def safe_extract(tar, path=".", members=None, numeric_owner=False):
                for member in tar.getmembers():
                    member_path = os.path.join(path, member.name)
                    if not is_within_directory(path, member_path):
                        raise Exception("Attempted Path Traversal in Tar File")

                tar.extractall(path, members, numeric_owner=numeric_owner)

            safe_extract(file, localstorage)

        os.remove(destination)

        print("Got it!\n")

    return bunny


# src/compas/__main__.py

# -*- coding: utf-8 -*-
from __future__ import print_function

import platform

try:
    import pkg_resources
except ImportError:
    pkg_resources = None

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(platform.python_version(), platform.python_implementation()))

    if pkg_resources:
        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 platform
import re
import shutil
import subprocess
import sys
import tempfile

try:
    NotADirectoryError
except NameError:

    class NotADirectoryError(Exception):
        pass


PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
SYMLINK_REGEX = re.compile(r"\n.*\<SYMLINKD\>\s(.*)\s\[(.*)\]\r")


__all__ = [
    "absjoin",
    "realpath",
    "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" == platform.python_implementation().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))


def realpath(path):
    """Return the canonical path of the specified filename, resolving any symbolic links encountered in the path.

    This function uses Python's stdlib `os.path.realpath` in most cases,
    except when inside IronPython because (guess what?) it is broken and
    doesn't really eliminate sym links, so, we fallback to a different
    way to identifying symlinks in that situation.
    """
    if not PY3 and is_ironpython():
        if is_windows():
            return _realpath_ipy_win(path)
        else:
            return _realpath_ipy_posix(path)

    return os.path.realpath(path)


def _realpath_ipy_win(path):
    dirname = os.path.basename(path)
    parent_path = os.path.join(path, "..")

    args = 'dir /c "{}" /Al'.format(parent_path)
    process = subprocess.Popen(args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    output, _error = process.communicate()
    matches = SYMLINK_REGEX.finditer(output)
    for match in matches:
        match_name = match.groups()[0].strip()
        match_link = match.groups()[1]

        if match_name == dirname:
            return match_link

    return path


def _realpath_ipy_posix(path):
    args = 'readlink -f "{}"'.format(path)
    process = subprocess.Popen(args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    output, _error = process.communicate()
    return output.strip()


# 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 = "/J" 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/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

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/geometry/__init__.py

from __future__ import absolute_import
import compas

# =============================================================================
# Core
# =============================================================================

from ._core._algebra import add_vectors, add_vectors_xy, allclose, argmax, argmin
from ._core._algebra import close, cross_vectors, cross_vectors_xy
from ._core._algebra import dehomogenize_vectors, divide_vectors, divide_vectors_xy, dot_vectors, dot_vectors_xy
from ._core._algebra import homogenize_vectors
from ._core._algebra import length_vector, length_vector_sqrd, length_vector_sqrd_xy, length_vector_xy
from ._core._algebra import multiply_matrices, multiply_matrix_vector, multiply_vectors, multiply_vectors_xy
from ._core._algebra import (
    norm_vector,
    norm_vectors,
    normalize_vector,
    normalize_vector_xy,
    normalize_vectors,
    normalize_vectors_xy,
)
from ._core._algebra import orthonormalize_vectors
from ._core._algebra import power_vector, power_vectors
from ._core._algebra import (
    scale_vector,
    scale_vector_xy,
    scale_vectors,
    scale_vectors_xy,
    square_vector,
    square_vectors,
    subtract_vectors,
    subtract_vectors_xy,
    sum_vectors,
)
from ._core._algebra import transpose_matrix
from ._core._algebra import (
    vector_average,
    vector_component,
    vector_component_xy,
    vector_standard_deviation,
    vector_variance,
)

from ._core.analytical import (
    archimedean_spiral_evaluate,
    circle_evaluate,
    ellipse_evaluate,
    helix_evaluate,
    logarithmic_spiral_evaluate,
)
from ._core.angles import (
    angle_planes,
    angle_points,
    angle_points_xy,
    angle_vectors,
    angle_vectors_signed,
    angle_vectors_xy,
    angles_points,
    angles_points_xy,
    angles_vectors,
    angles_vectors_xy,
)
from ._core.centroids import (
    centroid_points,
    centroid_points_weighted,
    centroid_points_xy,
    centroid_polygon,
    centroid_polygon_edges,
    centroid_polygon_edges_xy,
    centroid_polygon_vertices,
    centroid_polygon_vertices_xy,
    centroid_polygon_xy,
    centroid_polyhedron,
    midpoint_line,
    midpoint_line_xy,
    midpoint_point_point,
    midpoint_point_point_xy,
)
from ._core.constructors import circle_from_points, circle_from_points_xy
from ._core.distance import (
    closest_line_to_point,
    closest_point_in_cloud,
    closest_point_in_cloud_xy,
    closest_point_on_line,
    closest_point_on_line_xy,
    closest_point_on_plane,
    # closest_point_on_polygon_xy,
    closest_point_on_polyline,
    closest_point_on_polyline_xy,
    closest_point_on_segment,
    closest_point_on_segment_xy,
    # closest_points_in_cloud_numpy,
    distance_line_line,
    distance_point_line,
    distance_point_line_sqrd,
    distance_point_line_sqrd_xy,
    distance_point_line_xy,
    distance_point_plane,
    distance_point_plane_signed,
    distance_point_point,
    distance_point_point_sqrd,
    distance_point_point_sqrd_xy,
    distance_point_point_xy,
)
from ._core.normals import normal_polygon, normal_triangle, normal_triangle_xy
from ._core.quaternions import (
    quaternion_canonize,
    quaternion_conjugate,
    quaternion_is_unit,
    quaternion_multiply,
    quaternion_norm,
    quaternion_unitize,
)
from ._core.size import area_polygon, area_polygon_xy, area_triangle, area_triangle_xy, volume_polyhedron
from ._core.tangent import tangent_points_to_circle_xy

from ._core.kdtree import KDTree

from ._core.matrices import (
    axis_and_angle_from_matrix,
    axis_angle_from_quaternion,
    axis_angle_vector_from_matrix,
    basis_vectors_from_matrix,
    compose_matrix,
    decompose_matrix,
    euler_angles_from_matrix,
    euler_angles_from_quaternion,
    identity_matrix,
    matrix_determinant,
    matrix_from_axis_and_angle,
    matrix_from_axis_angle_vector,
    matrix_from_basis_vectors,
    matrix_from_change_of_basis,
    matrix_from_euler_angles,
    matrix_from_frame,
    matrix_from_frame_to_frame,
    matrix_from_orthogonal_projection,
    matrix_from_parallel_projection,
    matrix_from_perspective_entries,
    matrix_from_perspective_projection,
    matrix_from_quaternion,
    matrix_from_scale_factors,
    matrix_from_shear,
    matrix_from_shear_entries,
    matrix_from_translation,
    matrix_inverse,
    quaternion_from_axis_angle,
    quaternion_from_euler_angles,
    quaternion_from_matrix,
    translation_from_matrix,
)

from ._core.transformations import local_axes, local_to_world_coordinates
from ._core.transformations import (
    # mirror_point_line,
    # mirror_point_line_xy,
    mirror_point_plane,
    # mirror_point_point,
    # mirror_point_point_xy,
    mirror_points_line,
    mirror_points_line_xy,
    mirror_points_point,
    mirror_points_plane,
    mirror_points_point_xy,
    mirror_vector_vector,
)
from ._core.transformations import orient_points, orthonormalize_axes
from ._core.transformations import (
    project_point_line,
    project_point_line_xy,
    project_point_plane,
    project_points_line,
    project_points_line_xy,
    project_points_plane,
)
from ._core.transformations import reflect_line_plane, reflect_line_triangle, rotate_points, rotate_points_xy
from ._core.transformations import scale_points, scale_points_xy
from ._core.transformations import (
    transform_frames,
    transform_points,
    transform_vectors,
    translate_points_xy,
    translate_points,
)
from ._core.transformations import world_to_local_coordinates

if not compas.IPY:
    from ._core.transformations_numpy import dehomogenize_and_unflatten_frames_numpy, dehomogenize_numpy
    from ._core.transformations_numpy import homogenize_and_flatten_frames_numpy, homogenize_numpy
    from ._core.transformations_numpy import local_to_world_coordinates_numpy
    from ._core.transformations_numpy import (
        # transform_frames_numpy,
        transform_points_numpy,
        transform_vectors_numpy,
    )
    from ._core.transformations_numpy import world_to_local_coordinates_numpy

# =============================================================================
# Predicates
# =============================================================================

from .predicates.predicates_2 import (
    is_ccw_xy,
    is_colinear_xy,
    is_polygon_convex_xy,
    is_point_on_line_xy,
    is_point_on_segment_xy,
    is_point_on_polyline_xy,
    is_point_in_triangle_xy,
    is_point_in_polygon_xy,
    is_point_in_convex_polygon_xy,
    is_point_in_circle_xy,
    is_polygon_in_polygon_xy,
    is_intersection_line_line_xy,
    is_intersection_segment_segment_xy,
)
from .predicates.predicates_3 import (
    is_colinear,
    is_colinear_line_line,
    is_coplanar,
    is_polygon_convex,
    is_point_on_plane,
    is_point_infront_plane,
    is_point_behind_plane,
    is_point_in_halfspace,
    is_point_on_line,
    is_point_on_segment,
    is_point_on_polyline,
    is_point_in_triangle,
    is_point_in_circle,
    is_point_in_polyhedron,
    is_intersection_line_line,
    is_intersection_segment_segment,
    is_intersection_line_triangle,
    is_intersection_line_plane,
    is_intersection_segment_plane,
    is_intersection_plane_plane,
)

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

from .bbox.bbox import bounding_box, bounding_box_xy
from .bestfit.bestfit import bestfit_plane
from .booleans import (
    boolean_union_mesh_mesh,
    boolean_difference_mesh_mesh,
    boolean_intersection_mesh_mesh,
    boolean_union_polygon_polygon,
    boolean_difference_polygon_polygon,
    boolean_symmetric_difference_polygon_polygon,
    boolean_intersection_polygon_polygon,
)
from .hull.hull import convex_hull, convex_hull_xy
from .interpolation.barycentric import barycentric_coordinates
from .interpolation.coons import discrete_coons_patch
from .interpolation.tweening import tween_points, tween_points_distance
from .intersections import intersection_mesh_mesh, intersection_ray_mesh
from .intersections.intersections import (
    intersection_circle_circle_xy,
    intersection_ellipse_line_xy,
    intersection_line_box_xy,
    intersection_line_line_xy,
    intersection_line_line,
    intersection_line_plane,
    intersection_line_segment_xy,
    intersection_line_segment,
    intersection_line_triangle,
    intersection_plane_circle,
    intersection_plane_plane_plane,
    intersection_plane_plane,
    intersection_polyline_plane,
    intersection_segment_plane,
    intersection_segment_polyline_xy,
    intersection_segment_polyline,
    intersection_segment_segment_xy,
    intersection_segment_segment,
    intersection_sphere_line,
    intersection_sphere_sphere,
)
from .offset.offset import offset_line, offset_polyline, offset_polygon
from .quadmesh.planarization import quadmesh_planarize
from .triangulation import conforming_delaunay_triangulation, constrained_delaunay_triangulation, delaunay_triangulation
from .triangulation.delaunay import delaunay_from_points
from .triangulation.earclip import earclip_polygon
from .trimesh.curvature import trimesh_mean_curvature, trimesh_gaussian_curvature, trimesh_principal_curvature
from .trimesh.geodistance import trimesh_geodistance
from .trimesh.isolines import trimesh_isolines
from .trimesh.matrices import trimesh_massmatrix
from .trimesh.parametrisation import trimesh_harmonic, trimesh_lscm
from .trimesh.remesh import trimesh_remesh, trimesh_remesh_along_isoline, trimesh_remesh_constrained
from .trimesh.slicing import trimesh_slice

if not compas.IPY:
    from .bbox.bbox_numpy import (
        oriented_bounding_box_numpy,
        oriented_bounding_box_xy_numpy,
        oabb_numpy,
    )
    from .bestfit.bestfit_numpy import (
        bestfit_plane_numpy,
        bestfit_frame_numpy,
        bestfit_circle_numpy,
        bestfit_sphere_numpy,
    )
    from .hull.hull_numpy import convex_hull_numpy, convex_hull_xy_numpy
    from .icp.icp_numpy import icp_numpy
    from .triangulation.delaunay_numpy import delaunay_from_points_numpy, voronoi_from_points_numpy

# =============================================================================
# Class APIs
# =============================================================================

from .transformation import Transformation
from .projection import Projection
from .reflection import Reflection
from .rotation import Rotation
from .scale import Scale
from .shear import Shear
from .translation import Translation

from .geometry import Geometry
from .vector import Vector
from .point import Point
from .quaternion import Quaternion
from .frame import Frame
from .plane import Plane

# not sure what to do with line and polyline
# the required changes are drastic
from .pointcloud import Pointcloud

from .curves.curve import Curve
from .curves.line import Line
from .curves.polyline import Polyline
from .curves.circle import Circle
from .curves.ellipse import Ellipse
from .curves.parabola import Parabola
from .curves.hyperbola import Hyperbola
from .curves.arc import Arc
from .curves.nurbs import NurbsCurve

from .surfaces.surface import Surface
from .surfaces.spherical import SphericalSurface
from .surfaces.nurbs import NurbsSurface

from .shapes.shape import Shape
from .shapes.box import Box
from .shapes.capsule import Capsule
from .shapes.cone import Cone
from .shapes.cylinder import Cylinder
from .shapes.sphere import Sphere
from .shapes.torus import Torus

from .polygon import Polygon
from .polyhedron import Polyhedron

from .brep import (
    BrepError,
    BrepInvalidError,
    BrepTrimmingError,
)

from .brep.brep import Brep
from .brep.brep import BrepOrientation
from .brep.brep import BrepType
from .brep.edge import BrepEdge
from .brep.loop import BrepLoop
from .brep.face import BrepFace
from .brep.vertex import BrepVertex
from .brep.trim import BrepTrim
from .brep.trim import BrepTrimIsoStatus


__all__ = [
    "close",
    "allclose",
    "argmin",
    "argmax",
    "add_vectors",
    "add_vectors_xy",
    "sum_vectors",
    "cross_vectors",
    "cross_vectors_xy",
    "divide_vectors",
    "divide_vectors_xy",
    "dot_vectors",
    "dot_vectors_xy",
    "length_vector",
    "length_vector_xy",
    "length_vector_sqrd",
    "length_vector_sqrd_xy",
    "multiply_matrices",
    "multiply_matrix_vector",
    "multiply_vectors",
    "multiply_vectors_xy",
    "norm_vector",
    "norm_vectors",
    "normalize_vector",
    "normalize_vector_xy",
    "normalize_vectors",
    "normalize_vectors_xy",
    "homogenize_vectors",
    "dehomogenize_vectors",
    "orthonormalize_vectors",
    "power_vector",
    "power_vectors",
    "scale_vector",
    "scale_vector_xy",
    "scale_vectors",
    "scale_vectors_xy",
    "square_vector",
    "square_vectors",
    "subtract_vectors",
    "subtract_vectors_xy",
    "transpose_matrix",
    "vector_component",
    "vector_component_xy",
    "vector_average",
    "vector_variance",
    "vector_standard_deviation",
    "circle_evaluate",
    "ellipse_evaluate",
    "archimedean_spiral_evaluate",
    "logarithmic_spiral_evaluate",
    "helix_evaluate",
    "angles_vectors",
    "angles_vectors_xy",
    "angles_vectors",
    "angles_vectors_xy",
    "angles_points",
    "angles_points_xy",
    "angle_vectors",
    "angle_vectors_signed",
    "angle_vectors_xy",
    "angle_points",
    "angle_points_xy",
    "angle_planes",
    "midpoint_point_point",
    "midpoint_point_point_xy",
    "midpoint_line",
    "midpoint_line_xy",
    "centroid_points",
    "centroid_points_weighted",
    "centroid_points_xy",
    "centroid_polygon",
    "centroid_polygon_xy",
    "centroid_polygon_vertices",
    "centroid_polygon_vertices_xy",
    "centroid_polygon_edges",
    "centroid_polygon_edges_xy",
    "centroid_polyhedron",
    "circle_from_points",
    "circle_from_points_xy",
    "distance_point_point",
    "distance_point_point_xy",
    "distance_point_point_sqrd",
    "distance_point_point_sqrd_xy",
    "distance_point_line",
    "distance_point_line_xy",
    "distance_point_line_sqrd",
    "distance_point_line_sqrd_xy",
    "distance_point_plane",
    "distance_point_plane_signed",
    "distance_line_line",
    "closest_point_in_cloud",
    "closest_point_in_cloud_xy",
    "closest_point_on_line",
    "closest_point_on_line_xy",
    "closest_point_on_segment",
    "closest_point_on_segment_xy",
    "closest_point_on_polyline",
    "closest_point_on_polyline_xy",
    "closest_point_on_plane",
    "closest_line_to_point",
    "normal_polygon",
    "normal_triangle",
    "normal_triangle_xy",
    "quaternion_norm",
    "quaternion_unitize",
    "quaternion_is_unit",
    "quaternion_multiply",
    "quaternion_canonize",
    "quaternion_conjugate",
    "area_polygon",
    "area_polygon_xy",
    "area_triangle",
    "area_triangle_xy",
    "volume_polyhedron",
    "tangent_points_to_circle_xy",
    "bounding_box",
    "bounding_box_xy",
    "bestfit_plane",
    "boolean_union_mesh_mesh",
    "boolean_difference_mesh_mesh",
    "boolean_intersection_mesh_mesh",
    "boolean_union_polygon_polygon",
    "boolean_difference_polygon_polygon",
    "boolean_symmetric_difference_polygon_polygon",
    "boolean_intersection_polygon_polygon",
    "convex_hull",
    "convex_hull_xy",
    "barycentric_coordinates",
    "discrete_coons_patch",
    "tween_points",
    "tween_points_distance",
    "intersection_circle_circle_xy",
    "intersection_ellipse_line_xy",
    "intersection_line_box_xy",
    "intersection_line_line_xy",
    "intersection_line_line",
    "intersection_line_plane",
    "intersection_line_segment_xy",
    "intersection_line_segment",
    "intersection_line_triangle",
    "intersection_mesh_mesh",
    "intersection_plane_circle",
    "intersection_plane_plane_plane",
    "intersection_plane_plane",
    "intersection_polyline_plane",
    "intersection_ray_mesh",
    "intersection_segment_plane",
    "intersection_segment_polyline_xy",
    "intersection_segment_polyline",
    "intersection_segment_segment_xy",
    "intersection_segment_segment",
    "intersection_sphere_line",
    "intersection_sphere_sphere",
    "offset_line",
    "offset_polyline",
    "offset_polygon",
    "is_ccw_xy",
    "is_colinear_xy",
    "is_polygon_convex_xy",
    "is_point_on_line_xy",
    "is_point_on_segment_xy",
    "is_point_on_polyline_xy",
    "is_point_in_triangle_xy",
    "is_point_in_polygon_xy",
    "is_point_in_convex_polygon_xy",
    "is_point_in_circle_xy",
    "is_polygon_in_polygon_xy",
    "is_intersection_line_line_xy",
    "is_intersection_segment_segment_xy",
    "is_colinear",
    "is_colinear_line_line",
    "is_coplanar",
    "is_polygon_convex",
    "is_point_on_plane",
    "is_point_infront_plane",
    "is_point_behind_plane",
    "is_point_in_halfspace",
    "is_point_on_line",
    "is_point_on_segment",
    "is_point_on_polyline",
    "is_point_in_triangle",
    "is_point_in_circle",
    "is_point_in_polyhedron",
    "is_intersection_line_line",
    "is_intersection_segment_segment",
    "is_intersection_line_triangle",
    "is_intersection_line_plane",
    "is_intersection_segment_plane",
    "is_intersection_plane_plane",
    "quadmesh_planarize",
    "matrix_determinant",
    "matrix_inverse",
    "decompose_matrix",
    "compose_matrix",
    "identity_matrix",
    "matrix_from_frame",
    "matrix_from_frame_to_frame",
    "matrix_from_change_of_basis",
    "matrix_from_euler_angles",
    "matrix_from_axis_and_angle",
    "matrix_from_axis_angle_vector",
    "matrix_from_basis_vectors",
    "matrix_from_translation",
    "matrix_from_orthogonal_projection",
    "matrix_from_parallel_projection",
    "matrix_from_perspective_projection",
    "matrix_from_perspective_entries",
    "matrix_from_shear_entries",
    "matrix_from_shear",
    "matrix_from_scale_factors",
    "matrix_from_quaternion",
    "euler_angles_from_matrix",
    "euler_angles_from_quaternion",
    "axis_and_angle_from_matrix",
    "axis_angle_vector_from_matrix",
    "axis_angle_from_quaternion",
    "quaternion_from_matrix",
    "quaternion_from_euler_angles",
    "quaternion_from_axis_angle",
    "basis_vectors_from_matrix",
    "translation_from_matrix",
    "local_axes",
    "orthonormalize_axes",
    "transform_points",
    "transform_vectors",
    "transform_frames",
    "local_to_world_coordinates",
    "world_to_local_coordinates",
    "translate_points",
    "translate_points_xy",
    "scale_points",
    "scale_points_xy",
    "rotate_points",
    "rotate_points_xy",
    "mirror_vector_vector",
    "mirror_points_point",
    "mirror_points_point_xy",
    "mirror_points_line",
    "mirror_points_line_xy",
    "mirror_point_plane",
    "mirror_points_plane",
    "project_point_plane",
    "project_points_plane",
    "project_point_line",
    "project_point_line_xy",
    "project_points_line",
    "project_points_line_xy",
    "reflect_line_plane",
    "reflect_line_triangle",
    "orient_points",
    "conforming_delaunay_triangulation",
    "constrained_delaunay_triangulation",
    "delaunay_from_points",
    "delaunay_from_points",
    "delaunay_triangulation",
    "earclip_polygon",
    "trimesh_gaussian_curvature",
    "trimesh_mean_curvature",
    "trimesh_principal_curvature",
    "trimesh_geodistance",
    "trimesh_isolines",
    "trimesh_massmatrix",
    "trimesh_harmonic",
    "trimesh_lscm",
    "trimesh_remesh",
    "trimesh_remesh_constrained",
    "trimesh_remesh_along_isoline",
    "trimesh_slice",
    "Geometry",
    # "Bezier",
    "Arc",
    "Circle",
    "Ellipse",
    "Hyperbola",
    "Parabola",
    "Frame",
    "Line",
    "Plane",
    "Point",
    "Polygon",
    "Polyline",
    "Quaternion",
    "Vector",
    "Shape",
    "Box",
    "Capsule",
    "Cone",
    "Cylinder",
    "Polyhedron",
    "Sphere",
    "Torus",
    "Pointcloud",
    "KDTree",
    "Projection",
    "Reflection",
    "Rotation",
    "Scale",
    "Shear",
    "Transformation",
    "Translation",
    "Curve",
    "NurbsCurve",
    "Surface",
    "SphericalSurface",
    "NurbsSurface",
    "Brep",
    "BrepLoop",
    "BrepEdge",
    "BrepVertex",
    "BrepFace",
    "BrepTrim",
    "BrepTrimIsoStatus",
    "BrepType",
    "BrepOrientation",
    "BrepError",
    "BrepInvalidError",
    "BrepTrimmingError",
]

if not compas.IPY:
    __all__ += [
        "oriented_bounding_box_numpy",
        "oriented_bounding_box_xy_numpy",
        "oabb_numpy",
        "bestfit_plane_numpy",
        "bestfit_frame_numpy",
        "bestfit_circle_numpy",
        "bestfit_sphere_numpy",
        "convex_hull_numpy",
        "convex_hull_xy_numpy",
        "icp_numpy",
        "transform_points_numpy",
        "transform_vectors_numpy",
        "homogenize_numpy",
        "dehomogenize_numpy",
        "homogenize_and_flatten_frames_numpy",
        "dehomogenize_and_unflatten_frames_numpy",
        "world_to_local_coordinates_numpy",
        "local_to_world_coordinates_numpy",
        "delaunay_from_points_numpy",
        "voronoi_from_points_numpy",
    ]


# src/compas/geometry/frame.py

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

from compas.geometry import allclose
from compas.geometry import argmax
from compas.geometry import axis_angle_vector_from_matrix
from compas.geometry import basis_vectors_from_matrix
from compas.geometry import cross_vectors
from compas.geometry import decompose_matrix
from compas.geometry import euler_angles_from_matrix
from compas.geometry import matrix_from_axis_angle_vector
from compas.geometry import matrix_from_basis_vectors
from compas.geometry import matrix_from_euler_angles
from compas.geometry import matrix_from_quaternion
from compas.geometry import quaternion_from_matrix
from compas.geometry import subtract_vectors

from compas.geometry import Geometry
from compas.geometry import Transformation

from .vector import Vector
from .point import Point
from .quaternion import Quaternion


class Frame(Geometry):
    """A frame is defined by a base point and two orthonormal base vectors.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        The origin of the frame.
    xaxis : [float, float, float] | :class:`~compas.geometry.Vector`
        The x-axis of the frame.
    yaxis : [float, float, float] | :class:`~compas.geometry.Vector`
        The y-axis of the frame.

    Attributes
    ----------
    point : :class:`~compas.geometry.Point`
        The base point of the frame.
    xaxis : :class:`~compas.geometry.Vector`
        The local X axis of the frame.
    yaxis : :class:`~compas.geometry.Vector`
        The local Y axis of the frame.
    zaxis : :class:`~compas.geometry.Vector`, read-only
        The Z axis of the frame.
    normal : :class:`~compas.geometry.Vector`, read-only
        The normal of the base plane of the frame.
    quaternion : :class:`~compas.geometry.Quaternion`, read-only
        The quaternion from the rotation given by the frame.
    axis_angle_vector : :class:`~compas.geometry.Vector`, read-only
        The axis-angle vector representing the rotation of the frame.

    Notes
    -----
    All input vectors are orthonormalized when creating a frame, with the first
    vector as starting point.

    Examples
    --------
    >>> from compas.geometry import Point
    >>> from compas.geometry import Vector
    >>> f = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0])
    >>> f = Frame(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 1, 0))

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "point": Point.JSONSCHEMA,
            "xaxis": Vector.JSONSCHEMA,
            "yaxis": Vector.JSONSCHEMA,
        },
        "required": ["point", "xaxis", "yaxis"],
    }

    def __init__(self, point, xaxis, yaxis, **kwargs):
        super(Frame, self).__init__(**kwargs)
        self._point = None
        self._xaxis = None
        self._yaxis = None
        self.point = point
        self.xaxis = xaxis
        self.yaxis = yaxis

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {
            "point": self.point,
            "xaxis": self.xaxis,
            "yaxis": self.yaxis,
        }

    @data.setter
    def data(self, data):
        self.point = data["point"]
        self.xaxis = data["xaxis"]
        self.yaxis = data["yaxis"]

    @classmethod
    def from_data(cls, data):
        """Construct a frame from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> data = {'point': [0.0, 0.0, 0.0], 'xaxis': [1.0, 0.0, 0.0], 'yaxis': [0.0, 1.0, 0.0]}
        >>> frame = Frame.from_data(data)
        >>> frame.point
        Point(0.000, 0.000, 0.000)
        >>> frame.xaxis
        Vector(1.000, 0.000, 0.000)
        >>> frame.yaxis
        Vector(0.000, 1.000, 0.000)
        >>> frame.zaxis
        Vector(0.000, 0.000, 1.000)

        """
        return cls(data["point"], data["xaxis"], data["yaxis"])

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def point(self):
        if not self._point:
            raise ValueError("The frame has no origin.")
        return self._point

    @point.setter
    def point(self, point):
        self._point = Point(*point)

    @property
    def xaxis(self):
        if not self._xaxis:
            raise ValueError("The frame has no x-axis.")
        return self._xaxis

    @xaxis.setter
    def xaxis(self, vector):
        xaxis = Vector(*vector)
        xaxis.unitize()
        self._xaxis = xaxis

    @property
    def yaxis(self):
        if not self._yaxis:
            raise ValueError("The frame has no y-axis.")
        return self._yaxis

    @yaxis.setter
    def yaxis(self, vector):
        yaxis = Vector(*vector)
        yaxis.unitize()
        zaxis = Vector(*cross_vectors(self.xaxis, yaxis))
        zaxis.unitize()
        self._yaxis = Vector(*cross_vectors(zaxis, self.xaxis))

    @property
    def normal(self):
        return Vector(*cross_vectors(self.xaxis, self.yaxis))

    @property
    def zaxis(self):
        return self.normal

    @property
    def quaternion(self):
        R = matrix_from_basis_vectors(self.xaxis, self.yaxis)
        return Quaternion(*quaternion_from_matrix(R))

    @property
    def axis_angle_vector(self):
        R = matrix_from_basis_vectors(self.xaxis, self.yaxis)
        return Vector(*axis_angle_vector_from_matrix(R))

    # ==========================================================================
    # Customization
    # ==========================================================================

    def __repr__(self):
        return "Frame({0!r}, {1!r}, {2!r})".format(self.point, self.xaxis, self.yaxis)

    def __len__(self):
        return 3

    def __getitem__(self, key):
        if key == 0:
            return self.point
        if key == 1:
            return self.xaxis
        if key == 2:
            return self.yaxis
        raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.point = value
            return
        if key == 1:
            self.xaxis = value
            return
        if key == 2:
            self.yaxis = value
        raise KeyError

    def __iter__(self):
        return iter([self.point, self.xaxis, self.yaxis])

    def __eq__(self, other, tol=1e-05):
        if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other):
            return False
        return allclose(self, other)

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def worldXY(cls):
        """Construct the world XY frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The world XY frame.

        Examples
        --------
        >>> frame = Frame.worldXY()
        >>> frame.point
        Point(0.000, 0.000, 0.000)
        >>> frame.xaxis
        Vector(1.000, 0.000, 0.000)
        >>> frame.yaxis
        Vector(0.000, 1.000, 0.000)

        """
        return cls([0, 0, 0], [1, 0, 0], [0, 1, 0])

    @classmethod
    def worldZX(cls):
        """Construct the world ZX frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The world ZX frame.

        Examples
        --------
        >>> frame = Frame.worldZX()
        >>> frame.point
        Point(0.000, 0.000, 0.000)
        >>> frame.xaxis
        Vector(0.000, 0.000, 1.000)
        >>> frame.yaxis
        Vector(1.000, 0.000, 0.000)

        """
        return cls([0, 0, 0], [0, 0, 1], [1, 0, 0])

    @classmethod
    def worldYZ(cls):
        """Construct the world YZ frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The world YZ frame.

        Examples
        --------
        >>> frame = Frame.worldYZ()
        >>> frame.point
        Point(0.000, 0.000, 0.000)
        >>> frame.xaxis
        Vector(0.000, 1.000, 0.000)
        >>> frame.yaxis
        Vector(0.000, 0.000, 1.000)

        """
        return cls([0, 0, 0], [0, 1, 0], [0, 0, 1])

    @classmethod
    def from_points(cls, point, point_xaxis, point_xyplane):
        """Constructs a frame from 3 points.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            The origin of the frame.
        point_xaxis : [float, float, float] | :class:`~compas.geometry.Point`
            A point on the x-axis of the frame.
        point_xyplane : [float, float, float] | :class:`~compas.geometry.Point`
            A point within the xy-plane of the frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> frame = Frame.from_points([0, 0, 0], [1, 0, 0], [0, 1, 0])
        >>> frame.point
        Point(0.000, 0.000, 0.000)
        >>> frame.xaxis
        Vector(1.000, 0.000, 0.000)
        >>> frame.yaxis
        Vector(0.000, 1.000, 0.000)

        """
        xaxis = subtract_vectors(point_xaxis, point)
        xyvec = subtract_vectors(point_xyplane, point)
        yaxis = cross_vectors(cross_vectors(xaxis, xyvec), xaxis)
        return cls(point, xaxis, yaxis)

    @classmethod
    def from_rotation(cls, rotation, point=[0, 0, 0]):
        """Constructs a frame from a Rotation.

        Parameters
        ----------
        rotation : :class:`~compas.geometry.Rotation`
            The rotation defines the orientation of the frame.
        point : [float, float, float] | :class:`~compas.geometry.Point`, optional
            The origin of the frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> from compas.geometry import Rotation
        >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> R = Rotation.from_frame(f1)
        >>> f2 = Frame.from_rotation(R, point=f1.point)
        >>> f1 == f2
        True

        """
        xaxis, yaxis = rotation.basis_vectors
        return cls(point, xaxis, yaxis)

    @classmethod
    def from_transformation(cls, transformation):
        """Constructs a frame from a Transformation.

        Parameters
        ----------
        transformation : :class:`~compas.geometry.Transformation`
            The transformation defines the orientation of the frame through the
            rotation and the origin through the translation.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> from compas.geometry import Transformation
        >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(f1)
        >>> f2 = Frame.from_transformation(T)
        >>> f1 == f2
        True

        """
        xaxis, yaxis = transformation.basis_vectors
        point = transformation.translation_vector
        return cls(point, xaxis, yaxis)

    @classmethod
    def from_matrix(cls, matrix):
        """Construct a frame from a matrix.

        Parameters
        ----------
        matrix : list[list[float]]
            The 4x4 transformation matrix in row-major order.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> from compas.geometry import matrix_from_euler_angles
        >>> ea1 = [0.5, 0.4, 0.8]
        >>> M = matrix_from_euler_angles(ea1)
        >>> f = Frame.from_matrix(M)
        >>> ea2 = f.euler_angles()
        >>> allclose(ea1, ea2)
        True

        """
        _, _, angles, point, _ = decompose_matrix(matrix)
        R = matrix_from_euler_angles(angles, static=True, axes="xyz")
        xaxis, yaxis = basis_vectors_from_matrix(R)
        return cls(point, xaxis, yaxis)

    @classmethod
    def from_list(cls, values):
        """Construct a frame from a list of 12 or 16 float values.

        Parameters
        ----------
        values : list[float]
            The list of 12 or 16 values representing a 4x4 matrix.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Raises
        ------
        ValueError
            If the length of the list is neither 12 nor 16.

        Notes
        -----
        Since the transformation matrix follows the row-major order, the
        translational components must be at the list's indices 3, 7, 11.

        Examples
        --------
        >>> l = [-1.0,  0.0,  0.0, 8110, 0.0,  0.0, -1.0, 7020, 0.0, -1.0,  0.0, 1810]
        >>> f = Frame.from_list(l)

        """
        if len(values) == 12:
            values.extend([0.0, 0.0, 0.0, 1.0])
        if len(values) != 16:
            raise ValueError("Expected 12 or 16 floats but got %d" % len(values))

        matrix = [[0.0 for i in range(4)] for j in range(4)]
        for i in range(4):
            for j in range(4):
                matrix[i][j] = float(values[i * 4 + j])

        return cls.from_matrix(matrix)

    @classmethod
    def from_quaternion(cls, quaternion, point=[0, 0, 0]):
        """Construct a frame from a rotation represented by quaternion coefficients.

        Parameters
        ----------
        quaternion : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
            Four numbers that represent the four coefficient values of a quaternion.
        point : [float, float, float] | :class:`~compas.geometry.Point`, optional
            The point of the frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> q1 = [0.945, -0.021, -0.125, 0.303]
        >>> f = Frame.from_quaternion(q1, point=[1., 1., 1.])
        >>> q2 = f.quaternion
        >>> allclose(q1, q2, tol=1e-03)
        True

        """
        R = matrix_from_quaternion(quaternion)
        xaxis, yaxis = basis_vectors_from_matrix(R)
        return cls(point, xaxis, yaxis)

    @classmethod
    def from_axis_angle_vector(cls, axis_angle_vector, point=[0, 0, 0]):
        """Construct a frame from an axis-angle vector representing the rotation.

        Parameters
        ----------
        axis_angle_vector : [float, float, float]
            Three numbers that represent the axis of rotation and angle of
            rotation by its magnitude.
        point : [float, float, float] | :class:`~compas.geometry.Point`, optional
            The point of the frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> aav1 = [-0.043, -0.254, 0.617]
        >>> f = Frame.from_axis_angle_vector(aav1, point=[0, 0, 0])
        >>> aav2 = f.axis_angle_vector
        >>> allclose(aav1, aav2)
        True

        """
        R = matrix_from_axis_angle_vector(axis_angle_vector)
        xaxis, yaxis = basis_vectors_from_matrix(R)
        return cls(point, xaxis, yaxis)

    @classmethod
    def from_euler_angles(cls, euler_angles, static=True, axes="xyz", point=[0, 0, 0]):
        """Construct a frame from a rotation represented by Euler angles.

        Parameters
        ----------
        euler_angles : [float, float, float]
            Three numbers that represent the angles of rotations about the defined axes.
        static : bool, optional
            If True, the rotations are applied to a static frame.
            If False, to a rotational.
        axes : str, optional
            A 3 character string specifying the order of the axes.
        point : [float, float, float] | :class:`~compas.geometry.Point`, optional
            The point of the frame.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> ea1 = 1.4, 0.5, 2.3
        >>> f = Frame.from_euler_angles(ea1, static=True, axes='xyz')
        >>> ea2 = f.euler_angles(static=True, axes='xyz')
        >>> allclose(ea1, ea2)
        True

        """
        R = matrix_from_euler_angles(euler_angles, static, axes)
        xaxis, yaxis = basis_vectors_from_matrix(R)
        return cls(point, xaxis, yaxis)

    @classmethod
    def from_plane(cls, plane):
        """Constructs a frame from a plane.

        Xaxis and yaxis are arbitrarily selected based on the plane's normal.

        Parameters
        ----------
        plane : [point, vector] | :class:`~compas.geometry.Plane`
            A plane.

        Returns
        -------
        :class:`~compas.geometry.Frame`
            The constructed frame.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> plane = Plane([0,0,0], [0,0,1])
        >>> frame = Frame.from_plane(plane)
        >>> allclose(frame.normal, plane.normal)
        True

        """
        point, normal = plane
        # To construct a frame we need to find a vector v that is perpendicular
        # to the plane's normal. This means that the dot-product of v with the
        # normal must be equal to 0, which is true for the following vectors:
        vectors = [
            Vector(-normal[1], normal[0], 0),
            Vector(0, -normal[2], normal[1]),
            Vector(normal[2], 0, -normal[0]),
        ]
        # But if we are unlucky, one of these vectors is (0, 0, 0), so we
        # choose the vector with the longest length as xaxis.
        idx = argmax([v.length for v in vectors])
        xaxis = vectors[idx]
        yaxis = cross_vectors(normal, xaxis)
        return cls(point, xaxis, yaxis)

    # # ==========================================================================
    # # Static
    # # ==========================================================================

    # @staticmethod
    # def local_to_local_coordinates(frame1, frame2, object_in_frame1):
    #     """Returns the object's coordinates in frame1 in the local coordinates of frame2.

    #     Parameters
    #     ----------
    #     frame1 : [point, vector, vector] | :class:`~compas.geometry.Frame`
    #         A frame representing one local coordinate system.
    #     frame2 : [point, vector, vector] | :class:`~compas.geometry.Frame`
    #         A frame representing another local coordinate system.
    #     object_in_frame1 : [float, float, float] | :class:`~compas.geometry.Geometry`
    #         An object in the coordinate frame1.
    #         If you pass a list of float, it is assumed to represent a point.

    #     Returns
    #     -------
    #     :class:`~compas.geometry.Geometry`
    #         The object in the local coordinate system of frame2.

    #     Examples
    #     --------
    #     >>> from compas.geometry import Point
    #     >>> frame1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    #     >>> frame2 = Frame([2, 1, 3], [1., 0., 0.], [0., 1., 0.])
    #     >>> p1 = Point(2, 2, 2) # point in frame1
    #     >>> p2 = Frame.local_to_local_coordinates(frame1, frame2, p1) # point in frame2
    #     >>> Frame.local_to_local_coordinates(frame2, frame1, p2)
    #     Point(2.000, 2.000, 2.000)

    #     """
    #     T = Transformation.from_change_of_basis(frame1, frame2)
    #     if isinstance(object_in_frame1, (list, tuple)):
    #         return Point(*object_in_frame1).transformed(T)
    #     return object_in_frame1.transformed(T)

    # ==========================================================================
    # Conversions
    # ==========================================================================

    def to_transformation(self):
        """Convert the frame to a transformation.

        Returns
        -------
        :class:`compas.geometry.Transformation`
            The transformation.

        """
        return Transformation.from_frame(self)

    # ==========================================================================
    # Methods
    # ==========================================================================

    def euler_angles(self, static=True, axes="xyz"):
        """The Euler angles from the rotation given by the frame.

        Parameters
        ----------
        static : bool, optional
            If True the rotations are applied to a static frame.
            If False, to a rotational.
        axes : str, optional
            A 3 character string specifying the order of the axes.

        Returns
        -------
        list[float]
            Three numbers that represent the angles of rotations about the defined axes.

        Examples
        --------
        >>> ea1 = 1.4, 0.5, 2.3
        >>> f = Frame.from_euler_angles(ea1, static=True, axes='xyz')
        >>> ea2 = f.euler_angles(static=True, axes='xyz')
        >>> allclose(ea1, ea2)
        True

        """
        R = matrix_from_basis_vectors(self.xaxis, self.yaxis)
        return euler_angles_from_matrix(R, static, axes)

    def to_local_coordinates(self, obj_in_wcf):
        """Returns the object's coordinates in the local coordinate system of the frame.

        Parameters
        ----------
        obj_in_wcf : [float, float, float] | :class:`~compas.geometry.Geometry`
            An object in the world coordinate frame.

        Returns
        -------
        :class:`~compas.geometry.Geometry`
            The object in the local coordinate system of the frame.

        Notes
        -----
        If you pass a list of floats, it is assumed to represent a point.

        Examples
        --------
        >>> from compas.geometry import Point
        >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> pw = Point(2, 2, 2) # point in wcf
        >>> pl = frame.to_local_coordinates(pw) # point in frame
        >>> frame.to_world_coordinates(pl)
        Point(2.000, 2.000, 2.000)

        """
        T = Transformation.from_change_of_basis(Frame.worldXY(), self)
        if isinstance(obj_in_wcf, (list, tuple)):
            return Point(*obj_in_wcf).transformed(T)
        return obj_in_wcf.transformed(T)

    def to_world_coordinates(self, obj_in_lcf):
        """Returns the object's coordinates in the global coordinate frame.

        Parameters
        ----------
        obj_in_lcf : [float, float, float] | :class:`~compas.geometry.Geometry`
            An object in local coordinate system of the frame.

        Returns
        -------
        :class:`~compas.geometry.Geometry`
            The object in the world coordinate frame.

        Notes
        -----
        If you pass a list of floats, it is assumed to represent a point.

        Examples
        --------
        >>> from compas.geometry import Point
        >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> pl = Point(1.632, -0.090, 0.573) # point in frame
        >>> pw = frame.to_world_coordinates(pl) # point in wcf
        >>> frame.to_local_coordinates(pw)
        Point(1.632, -0.090, 0.573)

        """
        T = Transformation.from_change_of_basis(self, Frame.worldXY())
        if isinstance(obj_in_lcf, list):
            return Point(*obj_in_lcf).transformed(T)
        return obj_in_lcf.transformed(T)

    def transform(self, T):
        """Transform the frame.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation`
            The transformation.

        Examples
        --------
        >>> from compas.geometry import Transformation
        >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(f1)
        >>> f2 = Frame.worldXY()
        >>> f2.transform(T)
        >>> f1 == f2
        True

        """
        # replace this by function call
        X = T * Transformation.from_frame(self)
        point = X.translation_vector
        xaxis, yaxis = X.basis_vectors
        self.point = point
        self.xaxis = xaxis
        self.yaxis = yaxis


# src/compas/geometry/geometry.py

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

from compas.data import Data


class Geometry(Data):
    """Base class for all geometric objects."""

    def __init__(self, *args, **kwargs):
        super(Geometry, self).__init__(*args, **kwargs)

    def __ne__(self, other):
        # this is not obvious to ironpython
        return not self.__eq__(other)

    def transform(self, transformation):
        """Transform the geometry.

        Parameters
        ----------
        transformation : :class:`~compas.geometry.Transformation`
            The transformation used to transform the geometry.

        Returns
        -------
        None

        """
        raise NotImplementedError

    def transformed(self, transformation):
        """Returns a transformed copy of this geometry.

        Parameters
        ----------
        transformation : :class:`~compas.geometry.Transformation`
            The transformation used to transform the geometry.

        Returns
        -------
        :class:`Geometry`
            The transformed geometry.

        """
        geometry = self.copy()
        geometry.transform(transformation)
        return geometry


# src/compas/geometry/plane.py

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

from math import sqrt

from compas.geometry import cross_vectors
from compas.geometry import bestfit_plane
from compas.geometry import Geometry
from .vector import Vector
from .point import Point


class Plane(Geometry):
    """A plane is defined by a base point and a normal vector.

    Parameters
    ----------
    point : [float, float, float] | :class:`~compas.geometry.Point`
        The base point of the plane.
    normal : [float, float, float] | :class:`~compas.geometry.Vector`
        The normal vector of the plane.

    Attributes
    ----------
    point : :class:`~compas.geometry.Plane`
        The base point of the plane.
    normal : :class:`~compas.geometry.Vector`
        The normal vector of the plane.
    d : float, read-only
        The *d* parameter of the linear equation describing the plane.
    abcd : list[float], read-only
        The coefficients of the plane equation.

    Examples
    --------
    >>> plane = Plane([0, 0, 0], [0, 0, 1])
    >>> plane.point
    Point(0.000, 0.000, 0.000)
    >>> plane.normal
    Vector(0.000, 0.000, 1.000)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "point": Point.JSONSCHEMA,
            "normal": Vector.JSONSCHEMA,
        },
        "required": ["point", "normal"],
    }

    def __init__(self, point, normal, **kwargs):
        super(Plane, self).__init__(**kwargs)
        self._point = None
        self._normal = None
        self.point = point
        self.normal = normal

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"point": self.point, "normal": self.normal}

    @data.setter
    def data(self, data):
        self.point = data["point"]
        self.normal = data["normal"]

    @classmethod
    def from_data(cls, data):
        """Construct a plane from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Plane`
            The constructed plane.

        Examples
        --------
        >>> plane = Plane.from_data({'point': [0.0, 0.0, 0.0], 'normal': [0.0, 0.0, 1.0]})
        >>> plane.point
        Point(0.000, 0.000, 0.000)
        >>> plane.normal
        Vector(0.000, 0.000, 1.000)

        """
        return cls(data["point"], data["normal"])

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def point(self):
        if not self._point:
            raise ValueError("The plane has no point.")
        return self._point

    @point.setter
    def point(self, point):
        self._point = Point(*point)

    @property
    def normal(self):
        if not self._normal:
            raise ValueError("The plane has no normal.")
        return self._normal

    @normal.setter
    def normal(self, vector):
        self._normal = Vector(*vector)
        self._normal.unitize()

    @property
    def d(self):
        a, b, c = self.normal
        x, y, z = self.point
        return -a * x - b * y - c * z

    @property
    def abcd(self):
        a, b, c = self.normal
        d = self.d
        return a, b, c, d

    # ==========================================================================
    # Customization
    # ==========================================================================

    def __repr__(self):
        return "Plane({0!r}, {1!r})".format(self.point, self.normal)

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.point
        if key == 1:
            return self.normal
        raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.point = value
            return
        if key == 1:
            self.normal = value
            return
        raise KeyError

    def __iter__(self):
        return iter([self.point, self.normal])

    def __eq__(self, other):
        return self.point == other[0] and self.normal == other[1]

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_three_points(cls, a, b, c):
        """Construct a plane from three points in three-dimensional space.

        Parameters
        ----------
        a : [float, float, float] | :class:`~compas.geometry.Point`
            The first point.
        b : [float, float, float] | :class:`~compas.geometry.Point`
            The second point.
        c : [float, float, float] | :class:`~compas.geometry.Point`
            The second point.

        Returns
        -------
        :class:`~compas.geometry.Plane`
            A plane with base point `a` and normal vector defined as the unitized
            cross product of the vectors `ab` and `ac`.

        Examples
        --------
        >>> plane = Plane.from_three_points([0.0, 0.0, 0.0], [2.0, 1.0, 0.0], [0.0, 3.0, 0.0])
        >>> plane.point
        Point(0.000, 0.000, 0.000)
        >>> plane.normal
        Vector(0.000, 0.000, 1.000)

        """
        a = Point(*a)
        b = Point(*b)
        c = Point(*c)
        normal = Vector(*cross_vectors(b - a, c - a))
        return cls(a, normal)

    @classmethod
    def from_point_and_two_vectors(cls, point, u, v):
        """Construct a plane from a base point and two vectors.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            The base point.
        u : [float, float, float] | :class:`~compas.geometry.Vector`
            The first vector.
        v : [float, float, float] | :class:`~compas.geometry.Vector`
            The second vector.

        Returns
        -------
        :class:`~compas.geometry.Plane`
            A plane with base point `point` and normal vector defined as the unitized
            cross product of vectors `u` and `v`.

        Examples
        --------
        >>> plane = Plane.from_point_and_two_vectors([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
        >>> plane.point
        Point(0.000, 0.000, 0.000)
        >>> plane.normal
        Vector(0.000, 0.000, 1.000)

        """
        normal = Vector(*cross_vectors(u, v))
        return cls(point, normal)

    @classmethod
    def from_abcd(cls, abcd):
        """Construct a plane from the plane equation coefficients.

        Parameters
        ----------
        abcd : [float, float, float, float]
            The equation coefficients.

        Returns
        -------
        :class:`~compas.geometry.Plane`

        """
        a, b, c, d = abcd
        x = 1 / sqrt(a**2 + b**2 + c**2)
        normal = [a, b, c]
        point = [a * d * x, b * d * x, c * d * x]
        return cls(point, normal)

    @classmethod
    def worldXY(cls):
        """Construct the world XY plane.

        Returns
        -------
        :class:`~compas.geometry.Plane`
            The world XY plane.

        """
        return cls([0, 0, 0], [0, 0, 1])

    @classmethod
    def from_frame(cls, frame):
        """Construct a plane from a frame.

        Returns
        -------
        :class:`~compas.geometry.Plane`
            A plane with the frame's `point` and the frame's `normal`.

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> Plane.from_frame(frame)
        Plane(Point(1.000, 1.000, 1.000), Vector(-0.299, -0.079, 0.951))

        """
        return cls(frame.point, frame.normal)

    @classmethod
    def from_points(cls, points):
        """Construct a plane from a list of points.

        If the list contains more than three points, a plane is constructed that minimizes the distance to all points.

        Parameters
        ----------
        points : list of [float, float, float] | :class:`~compas.geometry.Point`
            The points.

        Returns
        -------
        :class:`~compas.geometry.Plane`
            The plane defined by the points.

        See Also
        --------
        :func:`compas.geometry.bestfit_plane`

        Examples
        --------
        >>> points = [[0.0, 0.0, 0.0], [2.0, 1.0, 0.0], [0.0, 3.0, 0.0]]
        >>> plane = Plane.from_points(points)
        >>> plane.point
        Point(0.000, 0.000, 0.000)
        >>> plane.normal
        Vector(0.000, 0.000, 1.000)

        """
        if len(points) == 3:
            return cls.from_three_points(*points)
        point, normal = bestfit_plane(points)
        return cls(point, normal)

    # ==========================================================================
    # Transformations
    # ==========================================================================

    def transform(self, T):
        """Transform this plane.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation` | list[list[float]]
            The transformation.

        Returns
        -------
        None

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> from compas.geometry import Transformation
        >>> from compas.geometry import Plane
        >>> f = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(f)
        >>> plane = Plane.worldXY()
        >>> plane.transform(T)

        """
        self.point.transform(T)
        self.normal.transform(T)

    # ==========================================================================
    # Methods
    # ==========================================================================

    def is_parallel(self, other, tol=1e-06):
        """Verify if this plane is parallel to another plane.

        Parameters
        ----------
        other : :class:`~compas.geometry.Plane`
            The other plane.
        tol : float, optional
            Tolerance for the dot product of the normals.

        Returns
        -------
        bool
            ``True`` if the planes are parallel.
            ``False`` otherwise.

        Examples
        --------
        >>> plane1 = Plane.worldXY()
        >>> plane2 = Plane([1.0, 1.0, 1.0], [0.0, 0.0, 1.0])
        >>> plane1.is_parallel(plane2)
        True

        """
        return abs(self.normal.dot(other.normal)) == 1 - tol

    def is_perpendicular(self, other, tol=1e-06):
        """Verify if this plane is perpendicular to another plane.

        Parameters
        ----------
        other : :class:`~compas.geometry.Plane`
            The other plane.
        tol : float, optional
            Tolerance for the dot product of the normals.

        Returns
        -------
        bool
            ``True`` if the planes are perpendicular.
            ``False`` otherwise.

        Examples
        --------
        >>> plane1 = Plane.worldXY()
        >>> plane2 = Plane([1.0, 1.0, 1.0], [0.0, 0.0, 1.0])
        >>> plane1.is_perpendicular(plane2)
        False

        """
        return abs(self.normal.dot(other.normal)) < tol

    def distance_to_point(self, point):
        """Compute the distance from a given point to the plane.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            The point.

        Returns
        -------
        float
            The distance from the point to the plane.

        Examples
        --------
        >>> plane = Plane.worldXY()
        >>> plane.distance_to_point([1.0, 1.0, 1.0])
        1.0

        """
        point = Point(*point)
        vector = point - self.point
        return self.normal.dot(vector)

    def closest_point(self, point):
        """Compute the closest point on the plane to a given point.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            The point.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The closest point on the plane.

        Examples
        --------
        >>> plane = Plane.worldXY()
        >>> plane.closest_point([1.0, 1.0, 1.0])
        Point(1.000, 1.000, 0.000)

        """
        point = Point(*point)
        vector = point - self.point
        distance = self.normal.dot(vector)
        return point + self.normal.scaled(-distance)

    def offset(self, distance):
        """Returns a new offset plane by a given distance.
        Plane normal is used as positive direction.

        Parameters
        ----------
        distance: float
            The offset distance.

        Returns
        -------
        :class:`~compas.geometry.Plane`
            The offset plane.

        """
        return Plane(self.point + self.normal.scaled(distance), self.normal)

    def intersection_with_line(self, line, tol=1e-06):
        """Compute the intersection of a plane and a line.

        Parameters
        ----------
        line : :class:`~compas.geometry.Line`
            The line.
        tol : float, optional
            Tolerance for the dot product of the line vector and the plane normal.

        Returns
        -------
        :class:`~compas.geometry.Point` | None
            The intersection point, or ``None`` if the line is parallel to the plane.

        Examples
        --------
        >>> plane = Plane.worldXY()
        >>> line = Line(Point(0, 0, 1), Vector(1, 1, 1))
        >>> plane.intersection_with_line(line)
        Point(0.000, 0.000, 0.000)

        """
        # The line is parallel to the plane
        if abs(self.normal.dot(line.vector)) < tol:
            return None

        t = (self.point - line.start).dot(self.normal) / line.vector.dot(self.normal)
        return line.point_at(t)

    def intersection_with_plane(self, plane):
        """Compute the intersection of two planes.

        Parameters
        ----------
        plane : :class:`~compas.geometry.Plane`
            The other plane.

        Returns
        -------
        :class:`~compas.geometry.Line` | :class:`~compas.geometry.Plane` | None
            The intersection line, or the intersection plane if the planes are
            parallel, or ``None`` if the planes are coincident.

        Examples
        --------
        >>> plane1 = Plane.worldXY()
        >>> plane2 = Plane([1.0, 1.0, 1.0], [0.0, 0.0, 1.0])
        >>> plane1.intersection(plane2)
        Line(Point(0.000, 0.000, 0.000), Vector(0.000, 0.000, 1.000))

        """
        from compas.geometry import Line

        if self.is_parallel(plane):
            return None

        # direction of the line
        direction = self.normal.cross(plane.normal)

        # point on the line
        line = Line(self.point, self.point + self.normal.cross(direction))
        point = plane.intersection_with_line(line)

        return Line(point, point + direction)


# src/compas/geometry/point.py

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

from compas import PRECISION

from compas.geometry import centroid_points
from compas.geometry import normal_polygon
from compas.geometry import distance_point_point
from compas.geometry import distance_point_line
from compas.geometry import distance_point_plane
from compas.geometry import is_point_on_segment
from compas.geometry import is_point_on_polyline
from compas.geometry import is_point_in_triangle
from compas.geometry import is_point_in_circle
from compas.geometry import is_point_in_polygon_xy
from compas.geometry import is_point_in_convex_polygon_xy
from compas.geometry import is_point_behind_plane
from compas.geometry import transform_points

from compas.geometry import Geometry
from .vector import Vector


class Point(Geometry):
    """A point is defined by XYZ coordinates.

    Parameters
    ----------
    x : float
        The X coordinate of the point.
    y : float
        The Y coordinate of the point.
    z : float, optional
        The Z coordinate of the point.

    Attributes
    ----------
    x : float
        The X coordinate of the point.
    y : float
        The Y coordinate of the point.
    z : float
        The Z coordinate of the point.

    Notes
    -----
    A `Point` object supports direct access to its xyz coordinates through
    the dot notation, as well list-style access using indices. Indexed
    access is implemented such that the `Point` behaves like a circular
    list [1]_.

    References
    ----------
    .. [1] Stack Overflow. *Pythonic Circular List*.
           Available at: https://stackoverflow.com/questions/8951020/pythonic-circular-list.

    Examples
    --------
    >>> p1 = Point(1, 2, 3)
    >>> p2 = Point(4, 5, 6)

    The XYZ coordinates of point objects can be accessed as object attributes or by trating the points as lists.

    >>> p1.x
    1.0
    >>> p1.y
    2.0
    >>> p1.z
    3.0
    >>> p1[0]
    1.0
    >>> p1[1]
    2.0
    >>> p1[2]
    3.0

    Point objects support basic arithmetic operations.

    >>> p1 + p2
    Point(5.000, 7.000, 9.000)
    >>> p1 * 2
    Point(2.000, 4.000, 6.000)
    >>> p1 ** 2
    Point(1.000, 4.000, 9.000)
    >>> p1
    Point(1.000, 2.000, 3.000)

    Points and lists can be used interchangeably.

    >>> p1 + [4, 5, 6]
    Point(5.000, 7.000, 9.000)

    Arithmetic operations can also be applied to modify a point object in-place.

    >>> p1 += p2
    >>> p1 *= 2
    >>> p1 **= 2
    >>> p1
    Point(100.000, 196.000, 324.000)

    """

    JSONSCHEMA = {
        "type": "array",
        "minItems": 3,
        "maxItems": 3,
        "items": {"type": "number"},
    }

    def __init__(self, x, y, z=0.0, **kwargs):
        super(Point, self).__init__(**kwargs)
        self._x = 0.0
        self._y = 0.0
        self._z = 0.0
        self.x = x
        self.y = y
        self.z = z

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        return list(self)

    @data.setter
    def data(self, data):
        self.x = data[0]
        self.y = data[1]
        self.z = data[2]

    @classmethod
    def from_data(cls, data):
        """Construct a point from a data dict.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The constructed point.

        Examples
        --------
        >>> point = Point.from_data([0.0, 0.0, 0.0])
        >>> point
        Point(0.000, 0.000, 0.000)

        """
        return cls(*data)

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, x):
        self._x = float(x)

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, y):
        self._y = float(y)

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self, z):
        self._z = float(z)

    # ==========================================================================
    # customization
    # ==========================================================================

    def __repr__(self):
        return "Point({0:.{3}f}, {1:.{3}f}, {2:.{3}f})".format(self.x, self.y, self.z, PRECISION[:1])

    def __len__(self):
        return 3

    def __getitem__(self, key):
        if isinstance(key, slice):
            return [self[i] for i in range(*key.indices(len(self)))]
        i = key % 3
        if i == 0:
            return self.x
        if i == 1:
            return self.y
        if i == 2:
            return self.z
        raise KeyError

    def __setitem__(self, key, value):
        i = key % 3
        if i == 0:
            self.x = value
            return
        if i == 1:
            self.y = value
            return
        if i == 2:
            self.z = value
            return
        raise KeyError

    def __iter__(self):
        return iter([self.x, self.y, self.z])

    def __eq__(self, other):
        """Is this point equal to the other point?

        Two points are considered equal if their XYZ coordinates are identical.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Point`
            The point to compare.

        Returns
        -------
        bool
            True if the points are equal.
            False otherwise.

        """
        return self.x == other[0] and self.y == other[1] and self.z == other[2]

    def __add__(self, other):
        """Return a point that is the sum of this point and another point.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Point`
            The point to add.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The resulting new point.

        """
        return Point(self.x + other[0], self.y + other[1], self.z + other[2])

    def __sub__(self, other):
        """Return a vector` that is the the difference between this point
        and another point.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Point`
            The point to subtract.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A vector from other to self.

        """
        x = self.x - other[0]
        y = self.y - other[1]
        z = self.z - other[2]
        return Vector(x, y, z)

    def __mul__(self, n):
        """Create a point from the coordinates of the current point multiplied
        by the given factor.

        Parameters
        ----------
        n : float
            The multiplication factor.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The resulting new point.

        """
        return Point(n * self.x, n * self.y, n * self.z)

    def __truediv__(self, n):
        """Create a point from the coordinates of the current point
        divided by the given factor.

        Parameters
        ----------
        n : float
            The multiplication factor.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The resulting new point.

        """
        return Point(self.x / n, self.y / n, self.z / n)

    def __pow__(self, n):
        """Create a point from the coordinates of the current point raised
        to the given power.

        Parameters
        ----------
        n : float
            The power.

        Returns
        -------
        :class:`~compas.geometry.Point`
            A new point with raised coordinates.

        """
        return Point(self.x**n, self.y**n, self.z**n)

    def __iadd__(self, other):
        """Add the coordinates of the other point to this point.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Point`
            The point to add.

        Returns
        -------
        None

        """
        self.x += other[0]
        self.y += other[1]
        self.z += other[2]
        return self

    def __isub__(self, other):
        """Subtract the coordinates of the other point from this point.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Point`
            The point to subtract.

        Returns
        -------
        None

        """
        self.x -= other[0]
        self.y -= other[1]
        self.z -= other[2]
        return self

    def __imul__(self, n):
        """Multiply the coordinates of this point by the given factor.

        Parameters
        ----------
        n : float
            The multiplication factor.

        Returns
        -------
        None

        """
        self.x *= n
        self.y *= n
        self.z *= n
        return self

    def __itruediv__(self, n):
        """Divide the coordinates of this point by the given factor.

        Parameters
        ----------
        n : float
            The multiplication factor.

        Returns
        -------
        None

        """
        self.x /= n
        self.y /= n
        self.z /= n
        return self

    def __ipow__(self, n):
        """Raise the coordinates of this point to the given power.

        Parameters
        ----------
        n : float
            The power.

        Returns
        -------
        None

        """
        self.x **= n
        self.y **= n
        self.z **= n
        return self

    # ==========================================================================
    # constructors
    # ==========================================================================

    # ==========================================================================
    # methods
    # ==========================================================================

    def distance_to_point(self, point):
        """Compute the distance to another point.

        Parameters
        ----------
        point : [float, float, float] | :class:`~compas.geometry.Point`
            The other point.

        Returns
        -------
        float
            The distance.

        Examples
        --------
        >>> a = Point(0.0, 0.0, 0.0)
        >>> b = Point(1.0, 0.0, 0.0)
        >>> a.distance_to_point(b)
        1.0

        """
        return distance_point_point(self, point)

    def distance_to_line(self, line):
        """Compute the distance to a line.

        Parameters
        ----------
        line : [point, point] | :class:`~compas.geometry.Line`
            The line.

        Returns
        -------
        float
            The distance.

        Examples
        --------
        >>> from compas.geometry import Line
        >>> point = Point(0.0, 0.0, 0.0)
        >>> line = Line(Point(1.0, 0.0, 0.0), Point(1.0, 1.0, 0.0))
        >>> point.distance_to_line(line)
        1.0

        """
        return distance_point_line(self, line)

    def distance_to_plane(self, plane):
        """Compute the distance to a plane.

        Parameters
        ----------
        plane : [point, vector] | :class:`~compas.geometry.Plane`
            The plane.

        Returns
        -------
        float
            The distance.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> from compas.geometry import Vector
        >>> point = Point(0.0, 0.0, 0.0)
        >>> plane = Plane(Point(1.0, 0.0, 0.0), Vector(1.0, 0.0, 0.0))
        >>> point.distance_to_plane(plane)
        1.0

        """
        return distance_point_plane(self, plane)

    # ==========================================================================
    # 2D predicates
    # ==========================================================================

    def in_polygon(self, polygon):
        """Determine if the point lies inside the given polygon.

        For this test, the point and polygon are assumed to lie in the XY plane.
        The Z coordinates of the point and of the points of the polygon are ignored.
        If the point and/or polygon do not lie in the XY plane, the result of this test is meaningless.
        It is up to the user to apply the necessary transformations beforehand.

        The polygon can be convex or concave.

        Parameters
        ----------
        polygon : sequence[point] | :class:`~compas.geometry.Polygon`
            The polygon.

        Returns
        -------
        bool
            True, if the point lies in the polygon.
            False, otherwise.

        See Also
        --------
        :meth:`in_convex_polygon`

        Examples
        --------
        >>> from compas.geometry import Polygon
        >>> poly = Polygon([Point(0.0, 0.0, 0.0), Point(1.0, 0.0, 0.0), Point(1.0, 1.0, 0.0), Point(0.0, 1.0, 0.0)])
        >>> point = Point(0.5, 0.5, 0.0)
        >>> point.in_polygon(poly)
        True

        """
        return is_point_in_polygon_xy(self, polygon)

    def in_convex_polygon(self, polygon):
        """Determine if the point lies inside the given convex polygon.

        For this test, the point and polygon are assumed to lie in the XY plane.
        The Z coordinates of the point and of the points of the polygon are ignored.
        If the point and/or polygon do not lie in the XY plane, the result of this test is meaningless.
        It is up to the user to apply the necessary transformations beforehand.

        The polygon must be convex.
        However, the method will not check that this is indeed the case.

        Parameters
        ----------
        polygon : sequence[point] | :class:`~compas.geometry.Polygon`
            The polygon.

        Returns
        -------
        bool
            True, if the point lies in the polygon.
            False, otherwise.

        See Also
        --------
        :meth:`in_polygon`

        Examples
        --------
        >>> from compas.geometry import Polygon
        >>> poly = Polygon([Point(0.0, 0.0, 0.0), Point(1.0, 0.0, 0.0), Point(1.0, 1.0, 0.0), Point(0.0, 1.0, 0.0)])
        >>> point = Point(0.5, 0.5, 0.0)
        >>> point.in_convex_polygon(poly)
        True

        """
        return is_point_in_convex_polygon_xy(self, polygon)

    # ==========================================================================
    # 3D predicates
    # ==========================================================================

    def on_line(self, line, tol=1e-6):
        """Determine if the point lies on the given line.

        Parameters
        ----------
        line : [point, point] | :class:`~compas.geometry.Line`
            The line.
        tol : float, optional
            A tolerance value for the distance between the point and the line.

        Returns
        -------
        bool
            True, if the point lies on the line.
            False, otherwise.

        Examples
        --------
        >>> from compas.geometry import Line
        >>> line = Line(Point(1.0, 0.0, 0.0), Point(1.0, 1.0, 0.0))
        >>> point = line.point(1.5)
        >>> point.on_line(line)
        True

        """
        return self.distance_to_line(line) < tol

    def on_segment(self, segment, tol=1e-6):
        """Determine if the point lies on the given segment.

        Parameters
        ----------
        segment : [point, point] | :class:`~compas.geometry.Line`
            The segment.
        tol : float, optional
            A tolerance value for the distance between the point and the segment.

        Returns
        -------
        bool
            True, if the point lies on the segment.
            False, otherwise.

        Examples
        --------
        >>> from compas.geometry import Line
        >>> line = Line(Point(1.0, 0.0, 0.0), Point(1.0, 1.0, 0.0))
        >>> point = line.point(1.5)
        >>> point.on_segment(line)
        False

        """
        return is_point_on_segment(self, segment, tol=tol)

    def on_polyline(self, polyline):
        """Determine if the point lies on the given polyline.

        Parameters
        ----------
        polyline : sequence[point] | :class:`~compas.geometry.Polyline`
            The polyline.

        Returns
        -------
        bool
            True, if the point lies on the polyline.
            False, otherwise.

        Examples
        --------
        >>> from compas.geometry import Polyline
        >>> poly = Polyline([Point(0.0, 0.0, 0.0), Point(1.0, 0.0, 0.0), Point(2.0, 0.0, 0.0)])
        >>> point = poly.point(0.5)
        >>> point.on_polyline(poly)
        True

        """
        return is_point_on_polyline(self, polyline)

    def on_plane(self, plane, tol=1e-6):
        """Determine if the point lies on the given plane.

        Parameters
        ----------
        plane : :class:`~compas.geometry.Plane`
            The plane.
        tol : float, optional
            A tolerance value for the distance between the point and the plane.

        Returns
        -------
        bool
            True, if the point lies on the plane.
            False, otherwise.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> from compas.geometry import Vector
        >>> plane = Plane(Point(0.0, 0.0, 0.0), Vector(0.0, 0.0, 1.0))
        >>> point = Point(0.5, 0.5, 0.0)
        >>> point.on_plane(plane)
        True

        """
        return self.distance_to_plane(plane) < tol

    def on_circle(self, circle, tol=1e-6):
        """Determine if the point lies on the given circle.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`
            The circle.
        tol : float, optional
            A tolerance value for the distance between the point and the circle.

        Returns
        -------
        bool
            True, if the point lies on the circle.
            False, otherwise.

        """
        if not self.on_plane(circle.plane, tol):
            return False
        return self.distance_to_point(circle.center) < circle.radius + tol

    def on_curve(self, curve, tol=1e-6):
        """Determine if the point lies on the given curve.

        Parameters
        ----------
        curve : :class:`~compas.geometry.Curve`
            The curve.
        tol : float, optional
            A tolerance value for the distance between the point and the curve.

        Returns
        -------
        bool
            True, if the point lies on the curve.
            False, otherwise.

        """
        return self.distance_to_point(curve.closest_point(self)) < tol

    def in_triangle(self, triangle):
        """Determine if the point lies inside the given triangle.

        Parameters
        ----------
        triangle : [point, point, point] | :class:`~compas.geometry.Polygon`
            The triangle.

        Returns
        -------
        bool
            True, if the point lies in the triangle.
            False, otherwise.

        Examples
        --------
        >>> from compas.geometry import Polygon
        >>> tri = Polygon([Point(0.0, 0.0, 0.0), Point(1.0, 0.0, 0.0), Point(0.5, 1.0, 0.0)])
        >>> point = Point(0.5, 0.5, 0.0)
        >>> point.in_triangle(tri)
        True

        """
        return is_point_in_triangle(self, triangle)

    def in_circle(self, circle):
        """Determine if the point lies inside the given circle.

        Parameters
        ----------
        circle : :class:`~compas.geometry.Circle`
            The circle.

        Returns
        -------
        bool
            True, if the point lies inside the circle.
            False, otherwise.

        Examples
        --------
        >>> from compas.geometry import Vector
        >>> from compas.geometry import Plane
        >>> from compas.geometry import Circle
        >>> circle = Circle(Plane(Point(0.0, 0.0, 0.0), Vector(0.0, 0.0, 1.0)), 1.0)
        >>> point = Point(0.5, 0.0, 0.0)
        >>> point.in_circle(circle)
        True

        """
        return is_point_in_circle(self, (circle.plane, circle.radius))

    def in_polyhedron(self, polyhedron):
        """Determine if the point lies inside the given polyhedron.

        This method verifies that the point lies behind the planes of all the faces of the polyhedron.
        Therefore, if the polyhedron is not convex, or if the faces are not planar, the result of this test is meaningless.
        Convexity and planarity are not checked.

        Parameters
        ----------
        polyhedron : [vertices, faces] | :class:`~compas.geometry.Polyhedron`
            The polyhedron.

        Returns
        -------
        bool
            True, if the point lies on the polyline.
            False, otherwise.

        """
        vertices, faces = polyhedron
        polygons = [[vertices[index] for index in face] for face in faces]
        planes = [[centroid_points(polygon), normal_polygon(polygon)] for polygon in polygons]
        return all(is_point_behind_plane(self, plane) for plane in planes)

    # ==========================================================================
    # transformations
    # ==========================================================================

    def transform(self, T):
        """Transform this point.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation` | list[list[float]]
            The transformation matrix.

        Examples
        --------
        >>> from compas.geometry import Translation
        >>> point = Point(0.0, 0.0, 0.0)
        >>> T = Translation.from_vector([1.0, 1.0, 1.0])
        >>> point.transform(T)
        >>> point.x == 1.0
        True

        """
        point = transform_points([self], T)[0]
        self.x = point[0]
        self.y = point[1]
        self.z = point[2]


# src/compas/geometry/pointcloud.py

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

from random import uniform

from compas.geometry import transform_points
from compas.geometry import centroid_points
from compas.geometry import bounding_box
from compas.geometry import closest_point_in_cloud
from compas.geometry import Geometry
from compas.geometry import Point


class Pointcloud(Geometry):
    """Class for working with pointclouds.

    Parameters
    ----------
    points : sequence[point]
        A sequence of points to add to the cloud.
    **kwargs : dict[str, Any], optional
        Additional keyword arguments collected in a dict.

    Attributes
    ----------
    points : list[:class:`~compas.geometry.Point`]
        The points of the cloud.

    Examples
    --------
    >>>

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "points": {"type": "array", "items": Point.JSONSCHEMA, "minItems": 1},
        },
        "required": ["points"],
    }

    def __init__(self, points, **kwargs):
        super(Pointcloud, self).__init__(**kwargs)
        self._points = None
        self.points = points

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"points": self.points}

    @data.setter
    def data(self, data):
        self._points = data["points"]

    @classmethod
    def from_data(cls, data):
        return cls(data["points"])

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def points(self):
        if self._points is None:
            self._points = []
        return self._points

    @points.setter
    def points(self, points):
        self._points = [Point(*point) for point in points]

    @property
    def centroid(self):
        return centroid_points(self.points)

    @property
    def bounding_box(self):
        from compas.geometry import Box

        return Box.from_bounding_box(bounding_box(self.points))

    # ==========================================================================
    # Customization
    # ==========================================================================

    def __repr__(self):
        return "Pointcloud({0!r})".format(self.points)

    def __len__(self):
        return len(self.points)

    def __getitem__(self, key):
        if key > len(self) - 1:
            raise KeyError
        return self.points[key]

    def __setitem__(self, key, value):
        if key > len(self) - 1:
            raise KeyError
        self.points[key] = value

    def __iter__(self):
        return iter(self.points)

    def __eq__(self, other):
        """Is this pointcloud equal to the other pointcloud?

        Two pointclouds are considered equal if they have the same number of points
        and if the XYZ coordinates of the corresponding points are identical.

        Parameters
        ----------
        other : :class:`~compas.geometry.Pointcloud` | list[[float, float, float] | :class:`~compas.geometry.Point`]
            The pointcloud to compare.

        Returns
        -------
        bool
            True if the pointclouds are equal.
            False otherwise.

        """
        if len(self) != len(other):
            return False
        A = sorted(self, key=lambda point: (point[0], point[1], point[2]))
        B = sorted(other, key=lambda point: (point[0], point[1], point[2]))
        return all(a == b for a, b in zip(A, B))

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_ply(cls, filepath):
        """Construct a pointcloud from a PLY file.

        Parameters
        ----------
        filepath : str | bytes | os.PathLike
            Path of the PLY file.

        Returns
        -------
        :class:`~compas.geometry.Pointcloud`

        """
        from compas.files import PLY

        points = []
        ply = PLY(filepath)
        for vertex in ply.reader.vertices:  # type: ignore
            points.append([vertex["x"], vertex["y"], vertex["z"]])
        cloud = cls(points)
        return cloud

    @classmethod
    def from_pcd(cls, filepath):
        """Construct a pointcloud from a PCD file.

        Parameters
        ----------
        filepath : str | bytes | os.PathLike
            Path of the PCD file.

        Returns
        -------
        :class:`~compas.geometry.Pointcloud`

        """
        pass

    @classmethod
    def from_bounds(cls, x, y, z, n):
        """Construct a point cloud within a given box.

        Parameters
        ----------
        x : float | tuple[float, float]
            Size of the cloud in the X direction.
            If a single value, the size is (0, x).
            If a pair of values, the size is (x[0], x[1]).
        y : float | tuple[float, float]
            Size of the cloud in the Y direction.
            If a single value, the size is (0, y).
            If a pair of values, the size is (y[0], y[1]).
        z : float | tuple[float, float]
            Size of the cloud in the Z direction.
            If a single value, the size is (0, z).
            If a pair of values, the size is (z[0], z[1]).
        n : int
            The number of points in the cloud.

        Returns
        -------
        :class:`~compas.geometry.Pointcloud`

        Notes
        -----
        The XYZ coordinates of the `n` points are radnomly chosen within the provided `x`, `y`, and `z` bounds.
        Thererefor, there is no guarantee that the bounds are part of the resulting coordinates.

        Examples
        --------
        >>>

        """
        try:
            len(x)
        except TypeError:
            xmin = 0
            xmax = x
        else:
            xmin, xmax = x
        try:
            len(y)
        except TypeError:
            ymin = 0
            ymax = y
        else:
            ymin, ymax = y
        try:
            len(z)
        except TypeError:
            zmin = 0
            zmax = z
        else:
            zmin, zmax = z
        x = [uniform(xmin, xmax) for i in range(n)]
        y = [uniform(ymin, ymax) for i in range(n)]
        z = [uniform(zmin, zmax) for i in range(n)]
        return cls(list(map(list, zip(x, y, z))))

    @classmethod
    def from_box(cls, box, n):
        """Construct a point cloud within a given box.

        Parameters
        ----------
        box: :class:`~compas.geometry.Box`
            The axis aligned bounding box of the cloud.
        n: int
            The number of points in the cloud.

        Returns
        -------
        :class:`~compas.geometry.Pointcloud`

        Examples
        --------
        >>> from compas.geometry import Box
        >>> cloud = Pointcloud.from_box(Box.from_width_height_depth(10, 3, 5), 100)
        >>> all((-5 < x < +5) and (-2.5 < y < +2.5) and (-1.5 < z < +1.5) for x, y, z in cloud.points)
        True

        """
        points = box.points
        x, y, z = zip(*points)
        xmin, xmax = min(x), max(x)
        ymin, ymax = min(y), max(y)
        zmin, zmax = min(z), max(z)
        x = [uniform(xmin, xmax) for i in range(n)]
        y = [uniform(ymin, ymax) for i in range(n)]
        z = [uniform(zmin, zmax) for i in range(n)]
        return cls(list(map(list, zip(x, y, z))))

    # ==========================================================================
    # Transformations
    # ==========================================================================

    def transform(self, T):
        """Apply a transformation to the pointcloud.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation`
            The transformation.

        Returns
        -------
        None
            The cloud is modified in place.
        """
        for index, point in enumerate(transform_points(self.points, T)):
            self.points[index].x = point[0]
            self.points[index].y = point[1]
            self.points[index].z = point[2]

    # ==========================================================================
    # Methods
    # ==========================================================================

    def closest_point(self, point):
        """Compute the closest point on the pointcloud to a given point.

        Parameters
        ----------
        point : :class:`~compas.geometry.Point`
            The point.

        Returns
        -------
        :class:`~compas.geometry.Point`
            The closest point on the pointcloud.

        """
        distance, point, index = closest_point_in_cloud(point, self.points)
        return point


# src/compas/geometry/polygon.py

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

import math

from compas.utilities import pairwise

from compas.geometry import allclose
from compas.geometry import area_polygon

# from compas.geometry import cross_vectors
from compas.geometry import centroid_polygon
from compas.geometry import is_coplanar
from compas.geometry import is_polygon_convex
from compas.geometry import transform_points
from compas.geometry import earclip_polygon
from compas.geometry import bounding_box

# from compas.geometry import bestfit_plane
from compas.geometry import Geometry
from compas.geometry import Transformation
from compas.geometry import Point

# from compas.geometry import Vector
from compas.geometry import Plane
from compas.geometry import Frame
from compas.geometry import Line


class Polygon(Geometry):
    """A polygon is a geometric object defined by a sequence of points forming a closed loop.

    A polygon is defined by a sequence of points connected by line segments
    forming a closed boundary that separates its interior from the exterior.

    In the sequence of points, the first and last element are not the same.
    The existence of the closing edge is implied.
    The boundary should not intersect itself.

    Polygons are not necessarily planar by construction; they can be warped or skewed.

    The polygon does not have a coordinate frame.
    Its geometry is always defined with respect to the world coordinate system.

    Polygons can be constructed parametrically by the number of sides and a radius,
    but the geometry of the resulting object is defined by a list of explicit point locations.

    Parameters
    ----------
    points : list[[float, float, float] | :class:`~compas.geometry.Point`]
        An ordered list of points.

    Attributes
    ----------
    points : list of :class:`~compas.geometry.Point`
        The points of the polygon.
    lines : list of :class:`~compas.geometry.Line`, read-only
        The lines of the polygon.
    length : float, read-only
        The length of the boundary.
    centroid : :class:`~compas.geometry.Point`, read-only
        The centroid of the polygon.
    normal : :class:`~compas.geometry.Vector`, read-only
        The (average) normal of the polygon.
    area : float, read-only
        The area of the polygon.
    is_convex : bool, read-only
        True if the polygon is convex.
    is_planar : bool, read-only
        True if the polygon is planar.

    See Also
    --------
    :func:`compas.geometry.centroid_polygon`,
    :func:`compas.geometry.area_polygon`,
    :func:`compas.geometry.is_polygon_convex`,
    :func:`earclip_polygon`

    Examples
    --------
    >>> polygon = Polygon([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]])
    >>> polygon.centroid
    Point(0.500, 0.500, 0.000)
    >>> polygon.area
    1.0

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {"points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}},
        "required": ["points"],
    }

    def __init__(self, points, **kwargs):
        super(Polygon, self).__init__(**kwargs)
        self._points = []
        self._lines = []
        self._vertices = []
        self._faces = []
        self.points = points

    def __repr__(self):
        return "Polygon([{0}])".format(", ".join(["{0!r}".format(point) for point in self.points]))

    def __len__(self):
        return len(self.points)

    def __getitem__(self, key):
        return self.points[key]

    def __setitem__(self, key, value):
        self.points[key] = Point(*value)
        self._lines = None

    def __iter__(self):
        return iter(self.points)

    def __eq__(self, other):
        if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other):
            return False
        return allclose(self, other)

    # ==========================================================================
    # Data
    # ==========================================================================

    @property
    def data(self):
        return {"points": self.points}

    @data.setter
    def data(self, data):
        self.points = data["points"]

    # ==========================================================================
    # Properties
    # ==========================================================================

    @property
    def points(self):
        return self._points

    @points.setter
    def points(self, points):
        if points[-1] == points[0]:
            points = points[:-1]
        self._points = [Point(*xyz) for xyz in points]
        self._lines = None

    @property
    def lines(self):
        if not self._lines:
            self._lines = [Line(a, b) for a, b in pairwise(self.points + self.points[:1])]
        return self._lines

    @property
    def vertices(self):
        if not self._vertices:
            return self.points
        return self._vertices

    @property
    def faces(self):
        if not self._faces:
            return [list(range(len(self.points)))]
        return self._faces

    @property
    def length(self):
        return sum(line.length for line in self.lines)

    @property
    def centroid(self):
        point = centroid_polygon(self.points)
        return Point(*point)

    @property
    def normal(self):
        # o = self.centroid
        # points = self.points
        # a2 = 0
        # normals = []
        # for i in range(-1, len(points) - 1):
        #     p1 = points[i]
        #     p2 = points[i + 1]
        #     u = [p1[_] - o[_] for _ in range(3)]  # type: ignore
        #     v = [p2[_] - o[_] for _ in range(3)]  # type: ignore
        #     w = cross_vectors(u, v)
        #     a2 += sum(w[_] ** 2 for _ in range(3)) ** 0.5
        #     normals.append(w)
        # n = [sum(axis) / a2 for axis in zip(*normals)]
        # n = Vector(*n)
        # return n
        return self.plane.normal

    @property
    def plane(self):
        return Plane.from_points(self.points)

    @property
    def frame(self):
        return Frame.from_plane(self.plane)

    @property
    def area(self):
        return area_polygon(self.points)

    @property
    def is_convex(self):
        return is_polygon_convex(self.points)

    @property
    def is_planar(self):
        return is_coplanar(self.points)

    @property
    def bounding_box(self):
        from compas.geometry import Box

        return Box.from_bounding_box(bounding_box(self.points))

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_rectangle(cls, point, width, height):
        """Construct a rectangle.

        Parameters
        ----------
        point : :class:`compas.geometry.Point`
            The lower left corner of the rectangle.
        width : float
            The width of the rectangle.
        height : float
            The height of the rectangle.

        Returns
        -------
        :class:`compas.geometry.Polygon`
            A rectangle.

        """
        x, y, z = point
        points = [
            [x, y, z],
            [x + width, y, z],
            [x + width, y + height, z],
            [x, y + height, z],
        ]
        return cls(points)

    @classmethod
    def from_sides_and_radius_xy(cls, n, radius):
        """Construct a polygon from a number of sides and a radius.
        The resulting polygon is planar, equilateral and equiangular.

        Parameters
        ----------
        n : int
            The number of sides.
        radius : float
            The radius of the circle the polygon will be circumscribed to.

        Returns
        -------
        :class:`~compas.geometry.Polygon`
            The constructed polygon.

        Notes
        -----
        The first point of the polygon aligns with the Y-axis.
        The order of the polygon's points is counterclockwise.

        Examples
        --------
        >>> from compas.geometry import dot_vectors
        >>> from compas.geometry import subtract_vectors
        >>> pentagon = Polygon.from_sides_and_radius_xy(5, 1.0)
        >>> len(pentagon.lines) == 5
        True
        >>> len({round(line.length, 6) for line in pentagon.lines}) == 1
        True
        >>> dot_vectors(pentagon.normal, [0.0, 0.0, 1.0]) == 1
        True
        >>> centertofirst = subtract_vectors(pentagon.points[0], pentagon.centroid)
        >>> dot_vectors(centertofirst, [0.0, 1.0, 0.0]) == 1
        True

        """
        assert n >= 3, "Supplied number of sides must be at least 3!"
        points = []
        side = math.pi * 2 / n
        for i in range(n):
            point = [
                math.sin(side * (n - i)) * radius,
                math.cos(side * (n - i)) * radius,
                0.0,
            ]
            points.append(point)
        return cls(points)

    # =============================================================================
    # Conversions
    # =============================================================================

    def to_vertices_and_faces(self, earclip=False):
        """Returns a list of vertices and faces.

        Parameters
        ----------
        earclip : bool, optional
            Earclip the polygon before returning the vertices and faces.

        Returns
        -------
        list[list[float]], list[list[int]]
            A list of vertex locations, and a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if earclip:
            T = Transformation.from_change_of_basis(Frame.worldXY(), self.frame)
            points = transform_points(self.points, T)
            self._faces = earclip_polygon(points)
        return self.vertices, self.faces

    def to_mesh(self, earclip=False):
        """Returns a mesh representation of the polygon.

        Parameters
        ----------
        earclip : bool, optional
            Earclip the polygon before converting to a mesh.

        Returns
        -------
        :class:`compas.datastructures.Mesh`
            A mesh object.

        """
        from compas.datastructures import Mesh

        vertices, faces = self.to_vertices_and_faces(earclip=earclip)
        return Mesh.from_vertices_and_faces(vertices, faces)

    def to_brep(self):
        """Returns a brep representation of the polygon.

        Returns
        -------
        :class:`compas.geometry.Brep`
            A boundary representation of the polygon.

        """
        from compas.geometry import Brep

        return Brep.from_polygons([self])

    # =============================================================================
    # Transformations
    # =============================================================================

    def transform(self, T):
        """Transform this polygon.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation` | list[list[float]]
            The transformation.

        Returns
        -------
        None

        Examples
        --------
        >>> from math import radians
        >>> from compas.geometry import Rotation
        >>> polygon = Polygon.from_sides_and_radius_xy(4, 1.0)
        >>> R = Rotation.from_axis_and_angle([0.0, 0.0, 1.0], radians(45))
        >>> polygon.transform(R)
        >>> polygon.points[0]
        Point(-0.707, 0.707, 0.000)

        """
        for index, point in enumerate(transform_points(self.points, T)):
            self.points[index].x = point[0]
            self.points[index].y = point[1]
            self.points[index].z = point[2]

    # =============================================================================
    # Methods
    # =============================================================================

    def boolean_union(self, other):
        """Compute the boolean union of this polygon and another polygon.

        For this operation, both polygons are assumed to lie in the XY plane.
        Therefore, the Z components of the points defining the polygons are simply ignored.
        If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
        Otherwise the results are meaningless.

        Parameters
        ----------
        other : :class:`compas.geometry.Polygon`
            The other polygon.

        Returns
        -------
        :class:`compas.geometry.Polygon`
            The union polygon.

        """
        from compas.geometry import boolean_union_polygon_polygon

        coords = boolean_union_polygon_polygon(self, other)
        return Polygon([[x, y, 0] for x, y in coords])  # type: ignore

    def boolean_difference(self, other):
        """Compute the boolean difference of this polygon and another polygon.

        For this operation, both polygons are assumed to lie in the XY plane.
        Therefore, the Z components of the points defining the polygons are simply ignored.
        If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
        Otherwise the results are meaningless.

        Parameters
        ----------
        other : :class:`compas.geometry.Polygon`
            The other polygon.

        Returns
        -------
        :class:`compas.geometry.Polygon`
            The difference polygon.

        """
        from compas.geometry import boolean_difference_polygon_polygon

        coords = boolean_difference_polygon_polygon(self, other)
        return Polygon([[x, y, 0] for x, y in coords])  # type: ignore

    def boolean_symmetric_difference(self, other):
        """Compute the boolean symmetric difference of this polygon and another polygon.

        For this operation, both polygons are assumed to lie in the XY plane.
        Therefore, the Z components of the points defining the polygons are simply ignored.
        If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
        Otherwise the results are meaningless.

        Parameters
        ----------
        other : :class:`compas.geometry.Polygon`
            The other polygon.

        Returns
        -------
        list[:class:`compas.geometry.Polygon`]
            The resulting polygons.

        """
        from compas.geometry import boolean_symmetric_difference_polygon_polygon

        coordsets = boolean_symmetric_difference_polygon_polygon(self, other)
        polygons = [Polygon([[x, y, 0] for x, y in coords]) for coords in coordsets]  # type: ignore
        return polygons

    def boolean_intersection(self, other):
        """Compute the boolean intersection of this polygon and another polygon.

        For this operation, both polygons are assumed to lie in the XY plane.
        Therefore, the Z components of the points defining the polygons are simply ignored.
        If the polygons are not in the XY plane, it is the responibility of the user to transform them accordingly.
        Otherwise the results are meaningless.

        Parameters
        ----------
        other : :class:`compas.geometry.Polygon`
            The other polygon.

        Returns
        -------
        :class:`compas.geometry.Polygon`
            The intersection polygon.

        """
        from compas.geometry import boolean_intersection_polygon_polygon

        coords = boolean_intersection_polygon_polygon(self, other)
        return Polygon([[x, y, 0] for x, y in coords])  # type: ignore


# src/compas/geometry/polyhedron.py

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

from math import sqrt

from compas.utilities import pairwise
from compas.geometry import transform_points
from compas.geometry import Polygon
from .geometry import Geometry


def tetrahedron():
    faces = [[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]]
    vertices = []
    L = 2.0
    r = L * sqrt(6) / 4.0
    c = 1.0 / r
    for i in (-1, 1):
        i *= c
        vertices.append([i, 0.0, -c / sqrt(2)])
        vertices.append([0.0, i, +c / sqrt(2)])
    return vertices, faces


def hexahedron():
    faces = [
        [0, 3, 2, 1],
        [0, 1, 7, 6],
        [0, 6, 5, 3],
        [4, 2, 3, 5],
        [4, 7, 1, 2],
        [4, 5, 6, 7],
    ]
    vertices = []
    L = 1.0
    r = L * sqrt(3) / 2.0
    c = 1.0 / r
    for i in -1.0, +1.0:
        i *= c
        vertices.append([+i, +i, +i])
        vertices.append([-i, +i, +i])
        vertices.append([-i, -i, +i])
        vertices.append([+i, -i, +i])
    return vertices, faces


def octahedron():
    faces = [
        [0, 1, 5],
        [1, 3, 5],
        [3, 4, 5],
        [0, 5, 4],
        [0, 2, 1],
        [1, 2, 3],
        [3, 2, 4],
        [0, 4, 2],
    ]
    vertices = []
    L = sqrt(2)
    r = L * sqrt(2) / 2.0
    c = 1.0 / r
    for i in -1.0, +1.0:
        i *= c
        vertices.append([i, 0.0, 0.0])
        vertices.append([0.0, i, 0.0])
        vertices.append([0.0, 0.0, i])
    return vertices, faces


def dodecahedron():
    phi = 0.5 * (1 + sqrt(5))
    vertices = []
    faces = [
        [0, 13, 11, 1, 3],
        [0, 3, 2, 8, 10],
        [0, 10, 18, 12, 13],
        [1, 4, 7, 2, 3],
        [1, 11, 14, 5, 4],
        [2, 7, 9, 6, 8],
        [5, 15, 9, 7, 4],
        [5, 14, 17, 19, 15],
        [6, 16, 18, 10, 8],
        [6, 9, 15, 19, 16],
        [12, 17, 14, 11, 13],
        [12, 18, 16, 19, 17],
    ]
    L = 2.0 / phi
    r = L * phi * sqrt(3) / 2.0
    c = 1.0 / r
    for i in -1, +1:
        i *= c
        for j in -1, +1:
            j *= c
            vertices.append([0, i / phi, j * phi])
            vertices.append([i / phi, j * phi, 0])
            vertices.append([i * phi, 0, j / phi])
            for k in -1, +1:
                k *= c
                vertices.append([i * 1.0, j * 1.0, k * 1.0])
    return vertices, faces


def icosahedron():
    phi = (1 + sqrt(5)) / 2.0
    vertices = [
        (-1, phi, 0),
        (1, phi, 0),
        (-1, -phi, 0),
        (1, -phi, 0),
        (0, -1, phi),
        (0, 1, phi),
        (0, -1, -phi),
        (0, 1, -phi),
        (phi, 0, -1),
        (phi, 0, 1),
        (-phi, 0, -1),
        (-phi, 0, 1),
    ]
    faces = [
        # 5 faces around point 0
        [0, 11, 5],
        [0, 5, 1],
        [0, 1, 7],
        [0, 7, 10],
        [0, 10, 11],
        # Adjacent faces
        [1, 5, 9],
        [5, 11, 4],
        [11, 10, 2],
        [10, 7, 6],
        [7, 1, 8],
        # 5 faces around 3
        [3, 9, 4],
        [3, 4, 2],
        [3, 2, 6],
        [3, 6, 8],
        [3, 8, 9],
        # Adjacent faces
        [4, 9, 5],
        [2, 4, 11],
        [6, 2, 10],
        [8, 6, 7],
        [9, 8, 1],
    ]
    return vertices, faces


class Polyhedron(Geometry):
    """A polyhedron is a geometric object defined by its vertices and faces.

    Parameters
    ----------
    vertices : list[[float, float, float] | :class:`~compas.geometry.Point`]
        The point locations of the vertices of the polyhedron.
    faces : list[list[int]]
        The faces as a list of index lists.

    Attributes
    ----------
    vertices : list[list[float]]
        The XYZ coordinates of the vertices of the polyhedron.
    faces : list[list[int]]
        The faces of the polyhedron defined as lists of vertex indices.
    edges : list[tuple[int, int]], read-only
        The edges of the polyhedron as vertex index pairs.

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "vertices": {
                "type": "array",
                "minItems": 4,
                "items": {
                    "type": "array",
                    "minItems": 3,
                    "maxItems": 3,
                    "items": {"type": "number"},
                },
            },
            "faces": {
                "type": "array",
                "minItems": 4,
                "items": {
                    "type": "array",
                    "minItems": 3,
                    "items": {"type": "integer", "minimum": 0},
                },
            },
        },
        "required": ["vertices", "faces"],
    }

    def __init__(self, vertices, faces, **kwargs):
        super(Polyhedron, self).__init__(**kwargs)
        self._vertices = None
        self._faces = None
        self.vertices = vertices
        self.faces = faces

    def __repr__(self):
        return "<Polyhedron with {} vertices and {} faces>".format(len(self.vertices), len(self.faces))

    def __len__(self):
        return 2

    def __getitem__(self, key):
        if key == 0:
            return self.vertices
        elif key == 1:
            return self.faces
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.vertices = value
        elif key == 1:
            self.faces = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.vertices, self.faces])

    def __add__(self, other):
        """Compute the boolean union using the "+" operator of this shape and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Polyhedron`
            The solid to add.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The resulting solid.

        Examples
        --------
        >>> from compas.geometry import Box, Sphere
        >>> A = Box(size=2).to_polyhedron()
        >>> B = Sphere(point=[1, 1, 1], radius=1.0).to_polyhedron(u=16)
        >>> C = A + B  # doctest: +SKIP

        """
        from compas.geometry import boolean_union_mesh_mesh

        A = self.vertices, self.faces
        B = other.vertices, other.faces
        V, F = boolean_union_mesh_mesh(A, B)  # type: ignore
        return Polyhedron(V, F)

    def __sub__(self, other):
        """Compute the boolean difference using the "-" operator of this shape and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Polyhedron`
            The solid to subtract.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The resulting solid.

        Examples
        --------
        >>> from compas.geometry import Box, Sphere
        >>> A = Box(size=2).to_polyhedron()
        >>> B = Sphere(point=[1, 1, 1], radius=1.0).to_polyhedron(u=16)
        >>> C = A - B  # doctest: +SKIP

        """
        from compas.geometry import boolean_difference_mesh_mesh

        A = self.vertices, self.faces
        B = other.vertices, other.faces
        V, F = boolean_difference_mesh_mesh(A, B)  # type: ignore
        return Polyhedron(V, F)

    def __and__(self, other):
        """Compute the boolean intersection using the "&" operator of this shape and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Polyhedron`
            The solid to intersect with.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The resulting solid.

        Examples
        --------
        >>> from compas.geometry import Box, Sphere
        >>> A = Box(size=2).to_polyhedron()
        >>> B = Sphere(point=[1, 1, 1], radius=1.0).to_polyhedron(u=16)
        >>> C = A & B  # doctest: +SKIP

        """
        from compas.geometry import boolean_intersection_mesh_mesh

        A = self.vertices, self.faces
        B = other.vertices, other.faces
        V, F = boolean_intersection_mesh_mesh(A, B)  # type: ignore
        return Polyhedron(V, F)

    def __or__(self, other):
        """Compute the boolean union using the "|" operator of this shape and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Shape`
            The solid to add.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The resulting solid.

        Examples
        --------
        >>> from compas.geometry import Box, Sphere
        >>> A = Box(size=2).to_polyhedron()
        >>> B = Sphere(point=[1, 1, 1], radius=1.0).to_polyhedron(u=16)
        >>> C = A | B  # doctest: +SKIP

        """
        return self.__add__(other)

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        return {"vertices": self.vertices, "faces": self.faces}

    @data.setter
    def data(self, data):
        self.vertices = data["vertices"]
        self.faces = data["faces"]

    @classmethod
    def from_data(cls, data):
        """Construct a polyhedron from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The constructed polyhedron.

        Examples
        --------
        >>> from compas.geometry import Polyhedron
        >>> p = Polyhedron.from_platonicsolid(4)
        >>> q = Polyhedron.from_data(p.data)

        """
        return cls(**data)

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def vertices(self):
        if not self._vertices:
            self._vertices = []
        return self._vertices

    @vertices.setter
    def vertices(self, vertices):
        self._vertices = vertices

    @property
    def faces(self):
        if not self._faces:
            self._faces = []
        return self._faces

    @faces.setter
    def faces(self, faces):
        self._faces = faces

    @property
    def edges(self):
        seen = set()
        for face in self.faces:
            for u, v in pairwise(face + face[:1]):
                if (u, v) not in seen:
                    seen.add((u, v))
                    seen.add((v, u))
                    yield u, v

    @property
    def polygons(self):
        return [Polygon([self.vertices[index] for index in face]) for face in self.faces]

    @property
    def planes(self):
        return [polygon.plane for polygon in self.polygons]

    @property
    def is_convex(self):
        for plane in self.planes:
            for vertex in self.vertices:
                if plane.normal.dot(plane.point - vertex) < 0:
                    return False
        return True

    # ==========================================================================
    # Constructors
    # ==========================================================================

    @classmethod
    def from_platonicsolid(cls, f):
        """Construct a polyhedron from one of the platonic solids.

        A Platonic solid is a regular, convex polyhedron. It is constructed by
        congruent regular polygonal faces with the same number of faces meeting
        at each vertex [1]_.

        Parameters
        ----------
        f : {4, 6, 8, 12, 20}

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The constructed polyhedron.

        References
        ----------
        .. [1] Wikipedia. *Platonic solids*.
            Available at: https://en.wikipedia.org/wiki/Platonic_solid.

        """
        if f == 4:
            vertices, faces = tetrahedron()
        elif f == 6:
            vertices, faces = hexahedron()
        elif f == 8:
            vertices, faces = octahedron()
        elif f == 12:
            vertices, faces = dodecahedron()
        elif f == 20:
            vertices, faces = icosahedron()
        else:
            raise ValueError("The number of sides of a platonic solid must be one of: 4, 6, 8, 12, 20.")
        solid = cls(vertices, faces)
        return solid

    @classmethod
    def from_halfspaces(cls, halfspaces, interior_point):
        """Construct a polyhedron from its half-spaces and one interior point.

        Parameters
        ----------
        halfspaces : array-like
            The coefficients of the hgalfspace equations in normal form.
        interior_point : array-like
            A point on the interior.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> left = Plane([-1, 0, 0], [-1, 0, 0])
        >>> right = Plane([+1, 0, 0], [+1, 0, 0])
        >>> top = Plane([0, 0, +1], [0, 0, +1])
        >>> bottom = Plane([0, 0, -1], [0, 0, -1])
        >>> front = Plane([0, -1, 0], [0, -1, 0])
        >>> back = Plane([0, +1, 0], [0, +1, 0])

        >>> import numpy as np
        >>> halfspaces = np.array([left.abcd, right.abcd, top.abcd, bottom.abcd, front.abcd, back.abcd], dtype=float)
        >>> interior = np.array([0, 0, 0], dtype=float)

        >>> p = Polyhedron.from_halfspaces(halfspaces, interior)

        """
        from itertools import combinations
        from numpy import asarray
        from scipy.spatial import HalfspaceIntersection, ConvexHull  # type: ignore
        from compas.datastructures import Mesh, mesh_merge_faces
        from compas.geometry import length_vector, dot_vectors, cross_vectors

        halfspaces = asarray(halfspaces, dtype=float)
        interior_point = asarray(interior_point, dtype=float)
        hsi = HalfspaceIntersection(halfspaces, interior_point)
        hull = ConvexHull(hsi.intersections)
        mesh = Mesh.from_vertices_and_faces([hsi.intersections[i] for i in hull.vertices], hull.simplices)
        mesh.unify_cycles()
        to_merge = []
        for a, b in combinations(mesh.faces(), 2):
            na = mesh.face_normal(a)
            nb = mesh.face_normal(b)
            if dot_vectors(na, nb) >= 1:
                if length_vector(cross_vectors(na, nb)) < 1e-6:
                    to_merge.append([a, b])
        for faces in to_merge:
            mesh_merge_faces(mesh, faces)
        vertices, faces = mesh.to_vertices_and_faces()
        return cls(vertices, faces)

    @classmethod
    def from_planes(cls, planes):
        """Construct a polyhedron from intersecting planes.

        Parameters
        ----------
        planes : list[[point, normal] | :class:`~compas.geometry.Plane`]

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> left = Plane([-1, 0, 0], [-1, 0, 0])
        >>> right = Plane([+1, 0, 0], [+1, 0, 0])
        >>> top = Plane([0, 0, +1], [0, 0, +1])
        >>> bottom = Plane([0, 0, -1], [0, 0, -1])
        >>> front = Plane([0, -1, 0], [0, -1, 0])
        >>> back = Plane([0, +1, 0], [0, +1, 0])
        >>> p = Polyhedron.from_planes([left, right, top, bottom, front, back])

        """
        from compas.geometry import Plane
        from compas.geometry import centroid_points

        planes = [Plane(point, normal) for point, normal in planes]
        interior = centroid_points([plane.point for plane in planes])
        return cls.from_halfspaces([plane.abcd for plane in planes], interior)

    # =============================================================================
    # Conversions
    # =============================================================================

    def to_vertices_and_faces(self):
        """Returns a list of vertices and faces.

        Returns
        -------
        tuple[list[float], list[int]
            A list of vertices and a list of faces.

        """
        return self.vertices, self.faces

    def to_mesh(self):
        """Returns a mesh representation of the polyhedron.

        Returns
        -------
        :class:`compas.datastructures.Mesh`
            A mesh object.

        """
        from compas.datastructures import Mesh

        return Mesh.from_vertices_and_faces(self.vertices, self.faces)

    # =============================================================================
    # Transformations
    # =============================================================================

    def transform(self, transformation):
        """Transform the polyhedron.

        Parameters
        ----------
        transformation : :class:`~compas.geometry.Transformation`

        Returns
        -------
        None

        """
        self.vertices = transform_points(self.vertices, transformation)

    # =============================================================================
    # Methods
    # =============================================================================

    def is_closed(self):
        """Verify that the polyhedron forms a closed surface.

        Returns
        -------
        bool
            True if the polyhedron is closed.
            False otherwise.

        """
        mesh = self.to_mesh()
        return mesh.is_closed()

    def boolean_union(self, other):
        """Compute the boolean union of this polyhedron and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Polyhedron`
            The polyhedron to add.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The resulting polyhedron.

        Examples
        --------
        >>> from compas.geometry import Box, Sphere
        >>> A = Box(size=2).to_polyhedron()
        >>> B = Sphere(point=[1, 1, 1], radius=1.0).to_polyhedron(u=16)
        >>> C = A.boolean_union(B)

        """
        from compas.geometry import boolean_union_mesh_mesh

        A = self.vertices, self.faces
        B = other.vertices, other.faces
        V, F = boolean_union_mesh_mesh(A, B)  # type: ignore

        return Polyhedron(V, F)

    def boolean_difference(self, other):
        """Compute the boolean difference of this polyhedron and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Polyhedron`
            The polyhedron to subtract.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The resulting polyhedron.

        Examples
        --------
        >>> from compas.geometry import Box, Sphere
        >>> A = Box(size=2).to_polyhedron()
        >>> B = Sphere(point=[1, 1, 1], radius=1.0).to_polyhedron(u=16)
        >>> C = A.boolean_difference(B)

        """
        from compas.geometry import boolean_difference_mesh_mesh

        A = self.vertices, self.faces
        B = other.vertices, other.faces
        V, F = boolean_difference_mesh_mesh(A, B)  # type: ignore

        return Polyhedron(V, F)

    def boolean_intersection(self, other):
        """Compute the boolean intersection of this polyhedron and another.

        Parameters
        ----------
        other : :class:`~compas.geometry.Polyhedron`
            The polyhedron to intersect with.

        Returns
        -------
        :class:`~compas.geometry.Polyhedron`
            The resulting polyhedron.

        Examples
        --------
        >>> from compas.geometry import Box, Sphere
        >>> A = Box(size=2).to_polyhedron()
        >>> B = Sphere(point=[1, 1, 1], radius=1.0).to_polyhedron(u=16)
        >>> C = A.boolean_intersection(B)

        """
        from compas.geometry import boolean_intersection_mesh_mesh

        A = self.vertices, self.faces
        B = other.vertices, other.faces
        V, F = boolean_intersection_mesh_mesh(A, B)  # type: ignore

        return Polyhedron(V, F)


# src/compas/geometry/projection.py

"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""
from compas.utilities import flatten
from compas.geometry import allclose
from compas.geometry import decompose_matrix
from compas.geometry import matrix_from_orthogonal_projection
from compas.geometry import matrix_from_parallel_projection
from compas.geometry import matrix_from_perspective_projection
from compas.geometry import matrix_from_perspective_entries
from compas.geometry import Transformation


class Projection(Transformation):
    """Class representing a projection transformation.

    Parameters
    ----------
    matrix : list[list[float]], optional
        A 4x4 matrix (or similar) representing a projection transformation.
    check : bool, optional
        If ``True``, the provided matrix will be checked for validity.

    Raises
    ------
    ValueError
        If ``check`` is ``True`` and the provided transformation matrix is not a projection matrix.

    Examples
    --------
    >>>

    """

    def __init__(self, matrix=None, check=False):
        if matrix and check:
            _, _, _, _, perspective = decompose_matrix(matrix)
            if not allclose(flatten(matrix), flatten(matrix_from_perspective_entries(perspective))):
                raise ValueError("This is not a proper projection matrix.")
        super(Projection, self).__init__(matrix=matrix)

    def __repr__(self):
        return "Projection({0!r}, check=False)".format(self.matrix)

    @classmethod
    def from_plane(cls, plane):
        """Construct an orthogonal projection transformation to project onto a plane.

        Parameters
        ----------
        plane : [point, normal] | :class:`~compas.geometry.Plane`
            The plane to project onto.

        Returns
        -------
        :class:`~compas.geometry.Projection`
            An orthogonal projection transformation.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> point = [0, 0, 0]
        >>> normal = [0, 0, 1]
        >>> plane = Plane(point, normal)
        >>> P = Projection.from_plane(plane)

        """
        matrix = matrix_from_orthogonal_projection(plane)
        return cls(matrix)

    @classmethod
    def from_plane_and_direction(cls, plane, direction):
        """Construct a parallel projection transformation to project onto a plane along a specific direction.

        Parameters
        ----------
        plane : [point, normal] | :class:`~compas.geometry.Plane`
            The plane to project onto.
        direction : [float, float, float] | :class:`~compas.geometry.Vector`
            The direction of projection direction.

        Returns
        -------
        :class:`~compas.geometry.Projection`
            A parallel projection transformation.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> point = [0, 0, 0]
        >>> normal = [0, 0, 1]
        >>> plane = Plane(point, normal)
        >>> direction = [1, 1, 1]
        >>> P = Projection.from_plane_and_direction(plane, direction)

        """
        matrix = matrix_from_parallel_projection(plane, direction)
        return cls(matrix)

    @classmethod
    def from_plane_and_point(cls, plane, center_of_projection):
        """Construct a perspective projection transformation to project onto a plane along lines that emanate from a single point, called the center of projection.

        Parameters
        ----------
        plane : [point, normal] | :class:`~compas.geometry.Plane`
            The plane to project onto.
        center_of_projection : [float, float, float] | :class:`~compas.geometry.Point`
            The camera view point.

        Returns
        -------
        :class:`~compas.geometry.Projection`
            A perspective projection transformation.

        Examples
        --------
        >>> from compas.geometry import Plane
        >>> point = [0, 0, 0]
        >>> normal = [0, 0, 1]
        >>> plane = Plane(point, normal)
        >>> center_of_projection = [1, 1, 0]
        >>> P = Projection.from_plane_and_point(plane, center_of_projection)

        """
        matrix = matrix_from_perspective_projection(plane, center_of_projection)
        return cls(matrix)

    @classmethod
    def from_entries(cls, perspective_entries):
        """Constructs a perspective transformation by the perspective entries
        of a matrix.

        Parameters
        ----------
        perspective_entries : [float, float, float, float]
            The 4 perspective entries of a matrix.

        Returns
        -------
        :class:`~compas.geometry.Projection`
            A projection transformation.

        """
        matrix = matrix_from_perspective_entries(perspective_entries)
        return cls(matrix)


# src/compas/geometry/quaternion.py

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

import math

from compas.geometry import quaternion_multiply
from compas.geometry import quaternion_conjugate
from compas.geometry import quaternion_unitize
from compas.geometry import quaternion_canonize
from compas.geometry import quaternion_norm
from compas.geometry import quaternion_is_unit
from compas.geometry import quaternion_from_matrix
from compas.geometry import Geometry


class Quaternion(Geometry):
    r"""A quaternion is defined by 4 components, X, Y, Z, and W.

    Parameters
    ----------
    w : float
        The scalar (real) part of a quaternion.
    x, y, z : float
        Components of the vector (complex, imaginary) part of a quaternion.

    Attributes
    ----------
    w : float
        The W component of the quaternion.
    x : float
        The X component of the quaternion.
    y : float
        The Y component of the quaternion.
    z : float
        The Z component of the quaternion.
    wxyz : list[float], read-only
        Quaternion as a list of float in the 'wxyz' convention.
    xyzw : list[float], read-only
        Quaternion as a list of float in the 'xyzw' convention.
    norm : float, read-only
        The length (euclidean norm) of the quaternion.
    is_unit : bool, read-only
        True if the quaternion is unit-length.
        False otherwise.

    Notes
    -----
    The default convention to represent a quaternion :math:`q` in this module
    is by four real values :math:`w`, :math:`x`, :math:`y`, :math:`z`.
    The first value :math:`w` is the scalar (real) part,
    and :math:`x`, :math:`y`, :math:`z` form the vector (complex, imaginary) part [1]_, so that:

    .. math::

        q = w + xi + yj + zk

    where :math:`i, j, k` are basis components with following multiplication rules [2]_:

    .. math::

        \begin{align}
        ii &= jj = kk = ijk = -1 \\
        ij &= k, \quad ji = -k \\
        jk &= i, \quad kj = -i \\
        ki &= j, \quad ik = -j
        \end{align}

    Quaternions are associative but not commutative.

    **Quaternion as rotation.**

    A rotation through an angle :math:`\theta` around an axis
    defined by a euclidean unit vector :math:`u = u_{x}i + u_{y}j + u_{z}k`
    can be represented as a quaternion:

    .. math::

        q = cos(\frac{\theta}{2}) + sin(\frac{\theta}{2}) [u_{x}i + u_{y}j + u_{z}k]

    i.e.:

    .. math::

        \begin{align}
        w &= cos(\frac{\theta}{2}) \\
        x &= sin(\frac{\theta}{2}) u_{x} \\
        y &= sin(\frac{\theta}{2}) u_{y} \\
        z &= sin(\frac{\theta}{2}) u_{z}
        \end{align}

    For a quaternion to represent a rotation or orientation, it must be unit-length.
    A quaternion representing a rotation :math:`p` resulting from applying a rotation
    :math:`r` to a rotation :math:`q`, i.e.: :math:`p = rq`,
    is also unit-length.

    References
    ----------
    .. [1] http://mathworld.wolfram.com/Quaternion.html
    .. [2] http://mathworld.wolfram.com/HamiltonsRules.html
    .. [3] https://github.com/matthew-brett/transforms3d/blob/master/transforms3d/quaternions.py

    Examples
    --------
    >>> Q = Quaternion(1.0, 1.0, 1.0, 1.0).unitized()
    >>> R = Quaternion(0.0,-0.1, 0.2,-0.3).unitized()
    >>> P = R*Q
    >>> P.is_unit
    True

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "w": {"type": "number"},
            "x": {"type": "number"},
            "y": {"type": "number"},
            "z": {"type": "number"},
        },
        "required": ["w", "x", "y", "z"],
    }

    def __init__(self, w, x, y, z, **kwargs):
        super(Quaternion, self).__init__(**kwargs)
        self._w = None
        self._x = None
        self._y = None
        self._z = None
        self.w = w
        self.x = x
        self.y = y
        self.z = z

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        """dict : Representation of the quaternion as a dict containing only native Python objects."""
        return {"w": self.w, "x": self.x, "y": self.y, "z": self.z}

    @data.setter
    def data(self, data):
        self.w = data["w"]
        self.x = data["x"]
        self.y = data["y"]
        self.z = data["z"]

    @classmethod
    def from_data(cls, data):
        """Construct a quaternion from a data dict.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Quaternion`
            The constructed quaternion.

        Examples
        --------
        >>>

        """
        return cls(data["w"], data["x"], data["y"], data["z"])

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def w(self):
        return self._w

    @w.setter
    def w(self, w):
        self._w = float(w)

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, x):
        self._x = float(x)

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, y):
        self._y = float(y)

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self, z):
        self._z = float(z)

    @property
    def wxyz(self):
        return [self.w, self.x, self.y, self.z]

    @property
    def xyzw(self):
        return [self.x, self.y, self.z, self.w]

    @property
    def norm(self):
        return quaternion_norm(self)

    @property
    def is_unit(self):
        return quaternion_is_unit(self)

    # ==========================================================================
    # customization
    # ==========================================================================

    def __getitem__(self, key):
        if key == 0:
            return self.w
        if key == 1:
            return self.x
        if key == 2:
            return self.y
        if key == 3:
            return self.z
        raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.w = value
            return
        if key == 1:
            self.x = value
            return
        if key == 2:
            self.y = value
        if key == 3:
            self.z = value
        raise KeyError

    def __eq__(self, other, tol=1e-05):
        if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other):
            return False
        for v1, v2 in zip(self, other):
            if math.fabs(v1 - v2) > tol:
                return False
        return True

    def __iter__(self):
        return iter(self.wxyz)

    def __len__(self):
        return 4

    def __repr__(self):
        return "Quaternion({:.{prec}f}, {:.{prec}f}, {:.{prec}f}, {:.{prec}f})".format(
            self.w, self.x, self.y, self.z, prec=3
        )

    def __mul__(self, other):
        """Multiply operator for two quaternions.

        Parameters
        ----------
        other : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
            A Quaternion.

        Returns
        -------
        :class:`~compas.geometry.Quaternion`
            The product :math:`P = R * Q` of this quaternion (R) multiplied by other quaternion (Q).

        Notes
        -----
        Multiplication of two quaternions :math:`R * Q` can be interpreted as applying rotation R to an orientation Q,
        provided that both R and Q are unit-length.
        The result is also unit-length.
        Multiplication of quaternions is not commutative!

        Examples
        --------
        >>> Q = Quaternion(1.0, 1.0, 1.0, 1.0).unitized()
        >>> R = Quaternion(0.0,-0.1, 0.2,-0.3).unitized()
        >>> P = R*Q
        >>> P.is_unit
        True

        """
        p = quaternion_multiply(list(self), list(other))
        return Quaternion(*p)

    # ==========================================================================
    # constructors
    # ==========================================================================

    @classmethod
    def from_frame(cls, frame):
        """Creates a quaternion object from a frame.

        Parameters
        ----------
        frame : :class:`~compas.geometry.Frame`

        Returns
        -------
        :class:`~compas.geometry.Quaternion`
            The new quaternion.

        Examples
        --------
        >>> from compas.geometry import allclose
        >>> from compas.geometry import Frame
        >>> q = [1., -2., 3., -4.]
        >>> F = Frame.from_quaternion(q)
        >>> Q = Quaternion.from_frame(F)
        >>> allclose(Q.canonized(), quaternion_canonize(quaternion_unitize(q)))
        True

        """
        w, x, y, z = frame.quaternion
        return cls(w, x, y, z)

    @classmethod
    def from_matrix(cls, M):
        """Create a Quaternion from a transformation matrix.

        Parameters
        ----------
        M : list[list[float]]

        Returns
        -------
        :class:`~compas.geometry.Quaternion`
            The new quaternion.

        Examples
        --------
        >>> from compas.geometry import matrix_from_euler_angles
        >>> ea = [0.2, 0.6, 0.2]
        >>> M = matrix_from_euler_angles(ea)
        >>> Quaternion.from_matrix(M)
        Quaternion(0.949, 0.066, 0.302, 0.066)

        """
        return cls(*quaternion_from_matrix(M))

    @classmethod
    def from_rotation(cls, R):
        """Create a Quaternion from a Rotatation.

        Parameters
        ----------
        R : :class:`~compas.geometry.Rotation`

        Returns
        -------
        :class:`~compas.geometry.Quaternion`
            The new quaternion.

        Examples
        --------
        >>> from compas.geometry import Frame, Rotation
        >>> R = Rotation.from_frame(Frame.worldYZ())
        >>> Quaternion.from_rotation(R)
        Quaternion(0.500, 0.500, 0.500, 0.500)

        """
        return cls.from_matrix(R.matrix)

    # ==========================================================================
    # methods
    # ==========================================================================

    def unitize(self):
        """Scales the quaternion to make it unit-length.

        Returns
        -------
        None

        Examples
        --------
        >>> q = Quaternion(1.0, 1.0, 1.0, 1.0)
        >>> q.is_unit
        False
        >>> q.unitize()
        >>> q.is_unit
        True

        """
        qu = quaternion_unitize(self)
        self.w, self.x, self.y, self.z = qu

    def unitized(self):
        """Returns a quaternion with a unit-length.

        Returns
        -------
        :class:`~compas.geometry.Quaternion`

        Examples
        --------
        >>> q = Quaternion(1.0, 1.0, 1.0, 1.0)
        >>> q.is_unit
        False
        >>> p = q.unitized()
        >>> p.is_unit
        True

        """
        qu = quaternion_unitize(self)
        return Quaternion(*qu)

    def canonize(self):
        """Makes the quaternion canonic.

        Returns
        -------
        None

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> q = Quaternion.from_frame(Frame.worldZX())
        >>> q
        Quaternion(-0.500, 0.500, 0.500, 0.500)
        >>> q.canonize()
        >>> q
        Quaternion(0.500, -0.500, -0.500, -0.500)

        """
        qc = quaternion_canonize(self)
        self.w, self.x, self.y, self.z = qc  # type: ignore

    def canonized(self):
        """Returns a quaternion in canonic form.

        Returns
        -------
        :class:`~compas.geometry.Quaternion`
            A quaternion in canonic form.

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> q = Quaternion.from_frame(Frame.worldZX())
        >>> q
        Quaternion(-0.500, 0.500, 0.500, 0.500)
        >>> p = q.canonized()
        >>> p
        Quaternion(0.500, -0.500, -0.500, -0.500)

        """
        qc = quaternion_canonize(self)
        return Quaternion(*qc)  # type: ignore

    def conjugate(self):
        """Conjugate the quaternion.

        Returns
        -------
        None

        Examples
        --------
        >>> q = Quaternion(1.0, 1.0, 1.0, 1.0)
        >>> q.conjugate()
        >>> q
        Quaternion(1.000, -1.000, -1.000, -1.000)

        """
        qc = quaternion_conjugate(self)
        self.w, self.x, self.y, self.z = qc

    def conjugated(self):
        """Returns a conjugate quaternion.

        Returns
        -------
        :class:`~compas.geometry.Quaternion`
            The conjugated quaternion.

        Examples
        --------
        >>> q = Quaternion(1.0, 1.0, 1.0, 1.0)
        >>> p = q.conjugated()
        >>> q
        Quaternion(1.000, 1.000, 1.000, 1.000)
        >>> p
        Quaternion(1.000, -1.000, -1.000, -1.000)

        """
        qc = quaternion_conjugate(self)
        return Quaternion(*qc)


# src/compas/geometry/reflection.py

"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""

# from compas.utilities import flatten
# from compas.geometry import allclose
from compas.geometry import dot_vectors
from compas.geometry import cross_vectors
from compas.geometry import normalize_vector

# from compas.geometry import decompose_matrix
# from compas.geometry import matrix_from_perspective_entries
from compas.geometry import identity_matrix
from compas.geometry import Transformation


class Reflection(Transformation):
    """Class representing a reflection transformation.

    A reflection transformation mirrors points at a plane.

    Parameters
    ----------
    matrix : list[list[float]], optional
        A 4x4 matrix (or similar) representing a reflection.

    Examples
    --------
    >>> point = [1, 1, 1]
    >>> normal = [0, 0, 1]
    >>> R1 = Reflection.from_plane((point, normal))
    >>> R2 = Transformation([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 2], [0, 0, 0, 1]])
    >>> R1 == R2
    True

    """

    def __init__(self, matrix=None, check=False):
        if matrix and check:
            # _, _, _, _, perspective = decompose_matrix(matrix)
            # if not allclose(flatten(matrix), flatten(matrix_from_perspective_entries(perspective))):
            #     raise ValueError("This is not a proper reflection matrix.")
            pass
        super(Reflection, self).__init__(matrix=matrix)

    def __repr__(self):
        return "Reflection({0!r}, check=False)".format(self.matrix)

    @classmethod
    def from_plane(cls, plane):
        """Construct a reflection transformation that mirrors wrt the given plane.

        Parameters
        ----------
        plane : [point, vector] | :class:`~compas.geometry.Plane`
            The reflection plane.

        Returns
        -------
        :class:`~compas.geometry.Reflection`
            The reflection transformation.

        """
        point, normal = plane
        normal = normalize_vector((list(normal)))
        matrix = identity_matrix(4)
        for i in range(3):
            for j in range(3):
                matrix[i][j] -= 2.0 * normal[i] * normal[j]
        for i in range(3):
            matrix[i][3] = 2 * dot_vectors(point, normal) * normal[i]
        return cls(matrix)

    @classmethod
    def from_frame(cls, frame):
        """Construct a reflection transformation that mirrors wrt the given frame.

        Parameters
        ----------
        frame : [point, vector, vector] | :class:`~compas.geometry.Frame`

        Returns
        -------
        :class:`~compas.geometry.Reflection`
            The reflection transformation.

        """
        if isinstance(frame, (tuple, list)):
            point = frame[0]
            zaxis = cross_vectors(frame[1], frame[2])
        else:
            point = frame.point
            zaxis = frame.zaxis
        return cls.from_plane((point, zaxis))


# src/compas/geometry/rotation.py

"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""
from compas.utilities import flatten
from compas.geometry import normalize_vector
from compas.geometry import cross_vectors
from compas.geometry import length_vector
from compas.geometry import allclose
from compas.geometry import decompose_matrix
from compas.geometry import matrix_from_euler_angles
from compas.geometry import euler_angles_from_matrix
from compas.geometry import matrix_from_axis_and_angle
from compas.geometry import axis_and_angle_from_matrix
from compas.geometry import matrix_from_quaternion
from compas.geometry import matrix_from_frame
from compas.geometry import basis_vectors_from_matrix
from compas.geometry import Transformation


class Rotation(Transformation):
    """Class representing a rotation transformation.

    The class contains methods for converting rotation matrices to axis-angle
    representations, Euler angles, quaternion and basis vectors.

    Parameters
    ----------
    matrix : list[list[float]], optional
        A 4x4 matrix (or similar) representing a rotation.
    check : bool, optional
        If ``True``, the provided matrix will be checked for validity.

    Attributes
    ----------
    quaternion : :class:`~compas.geometry.Quaternion`, read-only
        The quaternion from the rotation.
    axis_and_angle : tuple[:class:`~compas.geometry.Vector`, float], read-only
        The axis and the angle of the rotation.
    axis_angle_vector : :class:`~compas.geometry.Vector`, read-only
        The axis-angle vector of the rotation.
    basis_vectors : tuple[:class:`~compas.geometry.Vector`, :class:`~compas.geometry.Vector`], read-only
        The basis vectors of the rotation.

    Raises
    ------
    ValueError
        If ``check`` is ``True`` and the provided transformation matrix is not a rotation.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> f1 = Frame([0, 0, 0], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    >>> R = Rotation.from_frame(f1)
    >>> args = False, 'xyz'
    >>> alpha, beta, gamma = R.euler_angles(*args)
    >>> xaxis, yaxis, zaxis = [1, 0, 0], [0, 1, 0], [0, 0, 1]
    >>> Rx = Rotation.from_axis_and_angle(xaxis, alpha)
    >>> Ry = Rotation.from_axis_and_angle(yaxis, beta)
    >>> Rz = Rotation.from_axis_and_angle(zaxis, gamma)
    >>> f2 = Frame.worldXY()
    >>> f1 == f2.transformed(Rx * Ry * Rz)
    True

    """

    def __init__(self, matrix=None, check=False):
        if matrix and check:
            _, _, angles, _, _ = decompose_matrix(matrix)
            if not allclose(flatten(matrix), flatten(matrix_from_euler_angles(angles))):
                raise ValueError("This is not a proper rotation matrix.")
        super(Rotation, self).__init__(matrix=matrix)

    def __repr__(self):
        return "Rotation({0!r}, check=False)".format(self.matrix)

    @property
    def quaternion(self):
        from compas.geometry import Quaternion

        return Quaternion.from_matrix(self.matrix)

    @property
    def axis_and_angle(self):
        from compas.geometry import Vector

        axis, angle = axis_and_angle_from_matrix(self.matrix)
        return Vector(*axis), angle

    @property
    def axis_angle_vector(self):
        axis, angle = self.axis_and_angle
        return axis.scaled(angle)

    @property
    def basis_vectors(self):
        from compas.geometry import Vector

        xaxis, yaxis = basis_vectors_from_matrix(self.matrix)
        return Vector(*xaxis), Vector(*yaxis)

    @classmethod
    def from_axis_and_angle(cls, axis, angle, point=[0, 0, 0]):
        """Construct a rotation transformation from a rotation axis and an angle and an optional point of rotation.

        The rotation is based on the right hand rule, i.e. anti-clockwise if the
        axis of rotation points towards the observer.

        Parameters
        ----------
        axis : [float, float, float] | :class:`~compas.geometry.Vector`
            Three numbers that represent the axis of rotation.
        angle : float
            The rotation angle in radians.
        point : [float, float, float] | :class:`~compas.geometry.Point`
            A point to perform a rotation around an origin other than [0, 0, 0].

        Returns
        -------
        :class:`~compas.geometry.Rotation`

        Notes
        -----
        The rotation is based on the right hand rule, i.e. anti-clockwise
        if the axis of rotation points towards the observer.

        Examples
        --------
        >>> axis1 = normalize_vector([-0.043, -0.254, 0.617])
        >>> angle1 = 0.1
        >>> R = Rotation.from_axis_and_angle(axis1, angle1)
        >>> axis2, angle2 = R.axis_and_angle
        >>> allclose(axis1, axis2)
        True
        >>> allclose([angle1], [angle2])
        True

        """
        matrix = matrix_from_axis_and_angle(axis, angle, point=point)
        return cls(matrix)

    @classmethod
    def from_basis_vectors(cls, xaxis, yaxis):
        """Construct a rotation transformation from basis vectors (= orthonormal vectors).

        Parameters
        ----------
        xaxis : [float, float, float] | :class:`~compas.geometry.Vector`
            The x-axis of the frame.
        yaxis : [float, float, float] | :class:`~compas.geometry.Vector`
            The y-axis of the frame.

        Returns
        -------
        :class:`~compas.geometry.Rotation`

        Examples
        --------
        >>> xaxis = [0.68, 0.68, 0.27]
        >>> yaxis = [-0.67, 0.73, -0.15]
        >>> R = Rotation.from_basis_vectors(xaxis, yaxis)

        """
        xaxis = normalize_vector(list(xaxis))
        yaxis = normalize_vector(list(yaxis))
        zaxis = cross_vectors(xaxis, yaxis)
        yaxis = cross_vectors(zaxis, xaxis)
        matrix = [
            [xaxis[0], yaxis[0], zaxis[0], 0],
            [xaxis[1], yaxis[1], zaxis[1], 0],
            [xaxis[2], yaxis[2], zaxis[2], 0],
            [0, 0, 0, 1],
        ]
        return cls(matrix)

    @classmethod
    def from_frame(cls, frame):
        """Construct a rotation transformationn from world XY to frame.

        Parameters
        ----------
        frame : [point, vector, vector] | :class:`~compas.geometry.Frame`
            A frame describing the targeted Cartesian coordinate system.

        Returns
        -------
        :class:`~compas.geometry.Rotation`

        Notes
        -----
        Creating a rotation from a frame means that we omit all translational
        components. If that is unwanted, use ``Transformation.from_frame(frame)``.

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(f1)
        >>> f2 = Frame.from_transformation(T)
        >>> f1 == f2
        True

        """
        matrix = matrix_from_frame(frame)
        matrix[0][3] = 0.0
        matrix[1][3] = 0.0
        matrix[2][3] = 0.0
        return cls(matrix)

    @classmethod
    def from_quaternion(cls, quaternion):
        """Construct a rotation transformation` from quaternion coefficients.

        Parameters
        ----------
        quaternion : [float, float, float, float] | :class:`~compas.geometry.Quaternion`
            Four numbers that represents the four coefficient values of a quaternion.

        Returns
        -------
        :class:`~compas.geometry.Rotation`

        Examples
        --------
        >>> from compas.geometry import allclose
        >>> q1 = [0.945, -0.021, -0.125, 0.303]
        >>> R = Rotation.from_quaternion(q1)
        >>> q2 = R.quaternion
        >>> allclose(q1, q2, tol=1e-3)
        True

        """
        matrix = matrix_from_quaternion(quaternion)
        return cls(matrix)

    @classmethod
    def from_axis_angle_vector(cls, axis_angle_vector, point=[0, 0, 0]):
        """Construct a rotation transformation from an axis-angle vector.

        Parameters
        ----------
        axis_angle_vector : [float, float, float] | :class:`~compas.geometry.Vector`
            Three numbers that represent the axis of rotation and angle of rotation through the vector's magnitude.
        point : [float, float, float] | :class:`~compas.geometry.Point`, optional
            A point to perform a rotation around an origin other than [0, 0, 0].

        Returns
        -------
        :class:`~compas.geometry.Rotation`

        Examples
        --------
        >>> from compas.geometry import allclose
        >>> aav1 = [-0.043, -0.254, 0.617]
        >>> R = Rotation.from_axis_angle_vector(aav1)
        >>> aav2 = R.axis_angle_vector
        >>> allclose(aav1, aav2)
        True

        """
        angle = length_vector(axis_angle_vector)
        return cls.from_axis_and_angle(axis_angle_vector, angle, point)

    # why is this repeated here?
    @classmethod
    def from_euler_angles(cls, euler_angles, static=True, axes="xyz", **kwargs):
        """Construct a rotation transformation from Euler angles.

        In 3D space any orientation can be achieved by composing three
        elemental rotations, rotations about the axes (x,y,z) of a coordinate
        system. A triple of Euler angles can be interpreted in 24 ways, which
        depends on if the rotations are applied to a static (extrinsic) or
        rotating (intrinsic) frame and the order of axes.

        Parameters
        ----------
        euler_angles: [float, float, float]
            Three numbers that represent the angles of rotations about the
            defined axes.
        static: bool, optional
            If True the rotations are applied to a static frame.
            If False, to a rotational.
        axes: str, optional
            A 3 character string specifying order of the axes.

        Returns
        -------
        :class:`~compas.geometry.Rotation`

        Examples
        --------
        >>> from compas.geometry import allclose
        >>> ea1 = 1.4, 0.5, 2.3
        >>> args = False, 'xyz'
        >>> R1 = Rotation.from_euler_angles(ea1, *args)
        >>> ea2 = R1.euler_angles(*args)
        >>> allclose(ea1, ea2)
        True

        >>> alpha, beta, gamma = ea1
        >>> xaxis, yaxis, zaxis = [1, 0, 0], [0, 1, 0], [0, 0, 1]
        >>> Rx = Rotation.from_axis_and_angle(xaxis, alpha)
        >>> Ry = Rotation.from_axis_and_angle(yaxis, beta)
        >>> Rz = Rotation.from_axis_and_angle(zaxis, gamma)
        >>> R2 = Rx * Ry * Rz
        >>> R1 == R2
        True

        """
        return super(Rotation, cls).from_euler_angles(euler_angles, static, axes)

    # split up into two properties
    # euler_angles
    # rotating_euler_angles
    # xyz seems irelevant
    # could be added to base Transformation
    # always relevant
    def euler_angles(self, static=True, axes="xyz"):
        """Returns Euler angles from the rotation according to specified
        axis sequence and rotation type.

        Parameters
        ----------
        static : bool, optional
            If True the rotations are applied to a static frame.
            If False, to a rotational.
        axes : str, optional
            A 3 character string specifying the order of the axes.

        Returns
        -------
        [float, float, float]
            The 3 Euler angles.

        Examples
        --------
        >>> from compas.geometry import allclose
        >>> ea1 = 1.4, 0.5, 2.3
        >>> args = False, 'xyz'
        >>> R1 = Rotation.from_euler_angles(ea1, *args)
        >>> ea2 = R1.euler_angles(*args)
        >>> allclose(ea1, ea2)
        True

        """
        return euler_angles_from_matrix(self.matrix, static, axes)


# src/compas/geometry/scale.py

"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""
from compas.utilities import flatten
from compas.geometry import allclose
from compas.geometry import multiply_matrices
from compas.geometry import decompose_matrix
from compas.geometry import matrix_from_scale_factors
from compas.geometry import matrix_from_frame
from compas.geometry import matrix_inverse
from compas.geometry import Transformation


class Scale(Transformation):
    """Class representing a scale transformation.

    Parameters
    ----------
    matrix : list[list[float]], optional
        A 4x4 matrix (or similar) representing a scaling.
    check : bool, optional
        If ``True``, the provided matrix will be checked for validity.

    Raises
    ------
    ValueError
        If ``check`` is ``True`` and the provided transformation matrix is not a scale matrix.

    Examples
    --------
    >>> S = Scale.from_factors([1, 2, 3])
    >>> S[0, 0] == 1
    True
    >>> S[1, 1] == 2
    True
    >>> S[2, 2] == 3
    True

    >>> from compas.geometry import Point, Frame
    >>> point = Point(2, 5, 0)
    >>> frame = Frame(point, (1, 0, 0), (0, 1, 0))
    >>> points = [point, Point(2, 10, 0)]
    >>> S = Scale.from_factors([2.] * 3, frame)
    >>> [p.transformed(S) for p in points]
    [Point(2.000, 5.000, 0.000), Point(2.000, 15.000, 0.000)]

    """

    def __init__(self, matrix=None, check=False):
        if matrix and check:
            scale, _, _, _, _ = decompose_matrix(matrix)
            if not allclose(flatten(matrix), flatten(matrix_from_scale_factors(scale))):
                raise ValueError("This is not a proper scale matrix.")
        super(Scale, self).__init__(matrix=matrix)

    def __repr__(self):
        return "Scale({0!r}, check=False)".format(self.matrix)

    @classmethod
    def from_factors(cls, factors, frame=None):
        """Construct a scale transformation from scale factors.

        Parameters
        ----------
        factors : [float, float, float]
            The scale factors along X, Y, Z.
        frame : [point, vector, vector] | :class:`~compas.geometry.Frame`, optional
            The anchor frame for the scaling transformation.

        Returns
        -------
        :class:`~compas.geometry.Scale`
            A scale transformation.

        Examples
        --------
        >>> from compas.geometry import Point, Frame
        >>> point = Point(2, 5, 0)
        >>> frame = Frame(point, (1, 0, 0), (0, 1, 0))
        >>> points = [point, Point(2, 10, 0)]
        >>> S = Scale.from_factors([2.] * 3, frame)
        >>> [p.transformed(S) for p in points]
        [Point(2.000, 5.000, 0.000), Point(2.000, 15.000, 0.000)]

        """
        matrix = matrix_from_scale_factors(factors)
        if frame:
            Tw = matrix_from_frame(frame)
            Tl = matrix_inverse(Tw)
            matrix = multiply_matrices(multiply_matrices(Tw, matrix), Tl)
        return cls(matrix)


# src/compas/geometry/shear.py

"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""
from compas.utilities import flatten
from compas.geometry import allclose
from compas.geometry import matrix_from_shear_entries
from compas.geometry import matrix_from_shear
from compas.geometry import decompose_matrix
from compas.geometry import Transformation


class Shear(Transformation):
    """Class representing a shear transformation.

    A point P is transformed by the shear matrix into P" such that
    the vector P-P" is parallel to the direction vector and its extent is
    given by the angle of P-P'-P", where P' is the orthogonal projection
    of P onto the shear plane.

    Parameters
    ----------
    matrix : list[list[float]], optional
        A 4x4 matrix (or similar) representing a shear transformation.
    check : bool, optional
        If ``True``, the provided matrix will be checked for validity.

    Raises
    ------
    ValueError
        If ``check`` is ``True`` and the provided transformation matrix is not a shear matrix.

    Examples
    --------
    >>>

    """

    def __init__(self, matrix=None, check=False):
        if matrix and check:
            _, shear, _, _, _ = decompose_matrix(matrix)
            if not allclose(flatten(matrix), flatten(matrix_from_shear_entries(shear))):
                raise ValueError("This is not a proper shear matrix.")
        super(Shear, self).__init__(matrix=matrix)

    def __repr__(self):
        return "Shear({0!r}, check=False)".format(self.matrix)

    @classmethod
    def from_angle_direction_plane(cls, angle, direction, plane):
        """Construct a shear transformation from an angle, direction and plane.

        Parameters
        ----------
        angle : float
            The angle in radians.
        direction : [float, float, float] | :class:`~compas.geometry.Vector`
            The direction vector as list of 3 numbers.
            It must be orthogonal to the normal vector (i.e. it must lie in the shear plane).
        plane : [point, vector] | :class:`~compas.geometry.Plane`
            The shear plane defined by a point and normal.

        Returns
        -------
        :class:`~compas.geometry.Shear`
            The shear transformation object.

        Raises
        ------
        ValueError
            If the shear direction does not lie in the shear plane.

        Examples
        --------
        >>> from compas.geometry import cross_vectors
        >>> angle = 0.1
        >>> direction = [0.1, 0.2, 0.3]
        >>> point = [4, 3, 1]
        >>> normal = cross_vectors(direction, [1, 0.3, -0.1])
        >>> S = Shear.from_angle_direction_plane(angle, direction, (point, normal))

        """
        point, normal = plane
        matrix = matrix_from_shear(angle, direction, point, normal)
        return cls(matrix)

    @classmethod
    def from_entries(cls, shear_entries):
        """Construct a shear transformation from the 3 factors for x-y, x-z, and y-z axes.

        Parameters
        ----------
        shear_factors : [float, float, float]
            The 3 shear factors for x-y, x-z, and y-z axes.

        Returns
        -------
        :class:`~compas.geometry.Shear`
            The shear transformation object.

        Examples
        --------
        >>> S = Shear.from_entries([1, 2, 3])

        """
        matrix = matrix_from_shear_entries(shear_entries)
        return cls(matrix)


# src/compas/geometry/transformation.py

"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""
import math

from compas.data import Data

from compas.geometry import multiply_matrices
from compas.geometry import transpose_matrix
from compas.geometry import basis_vectors_from_matrix
from compas.geometry import decompose_matrix
from compas.geometry import identity_matrix
from compas.geometry import matrix_determinant
from compas.geometry import matrix_from_euler_angles
from compas.geometry import matrix_from_frame
from compas.geometry import matrix_from_translation
from compas.geometry import matrix_inverse
from compas.geometry import translation_from_matrix


class Transformation(Data):
    """Class representing a general 4x4 transformation matrix.

    It is the base class for transformations like :class:`Rotation`,
    :class:`Translation`, :class:`Scale`, :class:`Reflection`,
    :class:`Projection` and :class:`Shear`.

    The class allows to concatenate Transformations by multiplication, to
    calculate the inverse transformation and to decompose a transformation into
    its components of rotation, translation, scale, shear, and perspective.
    The matrix follows the row-major order, such that translation components
    x, y, z are in the right column of the matrix, i.e. ``M[0][3], M[1][3],
    M[2][3] = x, y, z``.

    Parameters
    ----------
    matrix : list[list[float]], optional
        The 4x4 transformation matrix.

    Attributes
    ----------
    scale : :class:`~compas.geometry.Scale`, read-only
        The scale component of the transformation matrix.
    shear : :class:`~compas.geometry.Shear`, read-only
        The shear component of the transformation matrix.
    rotation : :class:`~compas.geometry.Rotation`, read-only
        The rotation component of the transformation matrix.
    translation : :class:`~compas.geometry.Translation`, read-only
        The translation component of the transformation matrix.
    projection : :class:`~compas.geometry.Projection`, read-only
        The projection component of the transformation matrix.
    translation_vector : :class:`~compas.geometry.Vector`, read-only
        The translation component of the transformation matrix as a translation vector.
    basis_vectors : tuple[:class:`~compas.geometry.Vector`, :class:`~compas.geometry.Vector`], read-only
        The basis vectors from the rotation component of the transformation matrix.
    list : list[float], read-only
        Flattens the 4x4 transformation matrix into a list of 16 numbers.
    determinant : float, read-only
        The determinant of the matrix of the transformation.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
    >>> T = Transformation.from_frame(f1)
    >>> Sc, Sh, R, Tl, P = T.decomposed()
    >>> Tinv = T.inverse()

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "matrix": {
                "type": "array",
                "items": {
                    "type": "array",
                    "items": {"type": "number"},
                    "minItems": 4,
                    "maxItems": 4,
                },
                "minItems": 4,
                "maxItems": 4,
            },
        },
        "required": ["matrix"],
    }

    def __init__(self, matrix=None):
        super(Transformation, self).__init__()
        if not matrix:
            matrix = identity_matrix(4)
        self.matrix = matrix

    # ==========================================================================
    # descriptors
    # ==========================================================================

    @property
    def data(self):
        return {"matrix": self.matrix}

    @data.setter
    def data(self, data):
        self.matrix = data["matrix"]

    @property
    def scale(self):
        S, H, R, T, P = self.decomposed()
        return S

    @property
    def shear(self):
        S, H, R, T, P = self.decomposed()
        return H

    @property
    def rotation(self):
        S, H, R, T, P = self.decomposed()
        return R

    @property
    def translation(self):
        S, H, R, T, P = self.decomposed()
        return T

    @property
    def projection(self):
        S, H, R, T, P = self.decomposed()
        return P

    @property
    def translation_vector(self):
        from compas.geometry import Vector

        vector = translation_from_matrix(self.matrix)
        return Vector(*vector)

    @property
    def basis_vectors(self):
        from compas.geometry import Vector

        x, y = basis_vectors_from_matrix(self.rotation.matrix)
        return Vector(*x), Vector(*y)

    @property
    def list(self):
        return [a for c in self.matrix for a in c]

    @property
    def determinant(self):
        return matrix_determinant(self.matrix)

    # ==========================================================================
    # customisation
    # ==========================================================================

    def __mul__(self, other):
        return self.concatenated(other)

    def __imul__(self, other):
        return self.concatenated(other)

    def __getitem__(self, key):
        i, j = key
        return self.matrix[i][j]

    def __setitem__(self, key, value):
        i, j = key
        self.matrix[i][j] = value

    def __iter__(self):
        return iter(self.matrix)

    def __eq__(self, other, tol=1e-05):
        try:
            A = self.matrix
            B = other.matrix
            for i in range(4):
                for j in range(4):
                    if math.fabs(A[i][j] - B[i][j]) > tol:
                        return False
            return True
        except BaseException:
            return False

    def __ne__(self, other):
        # this is not obvious to ironpython
        return not self.__eq__(other)

    def __repr__(self):
        return "Transformation({0!r})".format(self.matrix)

    def __str__(self):
        s = "[[%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[0]])
        s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[1]])
        s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[2]])
        s += " [%s]]\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[3]])
        return s

    def __len__(self):
        return len(self.matrix)

    # ==========================================================================
    # constructors
    # ==========================================================================

    @classmethod
    def from_matrix(cls, matrix):
        """Creates a transformation from a list[list[float]] object.

        Parameters
        ----------
        matrix : list[list[float]]
            The 4x4 transformation matrix.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The transformation.

        """
        return cls(matrix)

    @classmethod
    def from_list(cls, numbers):
        """Creates a transformation from a list of 16 numbers.

        Parameters
        ----------
        numbers : list[float]
            A list of 16 numbers

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The transformation.

        Notes
        -----
        Since the transformation matrix follows the row-major order, the
        translational components must be at the list's indices 3, 7, 11.

        Examples
        --------
        >>> numbers = [1, 0, 0, 3, 0, 1, 0, 4, 0, 0, 1, 5, 0, 0, 0, 1]
        >>> T = Transformation.from_list(numbers)

        """
        matrix = identity_matrix(4)
        for i in range(4):
            for j in range(4):
                matrix[i][j] = float(numbers[i * 4 + j])
        return cls(matrix)

    @classmethod
    def from_euler_angles(cls, euler_angles, static=True, axes="xyz", point=[0, 0, 0]):
        """Construct a transformation from a rotation represented by Euler angles.

        Parameters
        ----------
        euler_angles : [float, float, float]
            Three numbers that represent the angles of rotations about the defined axes.
        static : bool, optional
            If True the rotations are applied to a static frame.
            If False, to a rotational.
        axes : str, optional
            A 3 character string specifying the order of the axes.
        point : list[float], optional
            The point of the frame.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The transformation.

        """
        R = matrix_from_euler_angles(euler_angles, static, axes)
        T = matrix_from_translation(point)
        M = multiply_matrices(T, R)
        return cls.from_matrix(M)

    # should not one of the two just have a "to" function
    @classmethod
    def from_frame(cls, frame):
        """Construct a transformation from world XY to frame.

        Parameters
        ----------
        frame : :class:`~compas.geometry.Frame`
            A frame describing the targeted Cartesian coordinate system.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The transformation.

        Notes
        -----
        It is the same as from_frame_to_frame(Frame.worldXY(), frame).

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(f1)
        >>> f2 = Frame.from_transformation(T)
        >>> f1 == f2
        True

        """
        return cls(matrix_from_frame(frame))

    @classmethod
    def from_frame_to_frame(cls, frame_from, frame_to):
        """Construct a transformation between two frames.

        This transformation allows to transform geometry from one Cartesian
        coordinate system defined by `frame_from` to another Cartesian
        coordinate system defined by `frame_to`.

        Parameters
        ----------
        frame_from : :class:`~compas.geometry.Frame`
            A frame defining the original Cartesian coordinate system.
        frame_to : :class:`~compas.geometry.Frame`
            A frame defining the targeted Cartesian coordinate system.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The transformation.

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> f1 = Frame([2, 2, 2], [0.12, 0.58, 0.81], [-0.80, 0.53, -0.26])
        >>> f2 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame_to_frame(f1, f2)
        >>> f1.transform(T)
        >>> f1 == f2
        True

        """
        T1 = cls.from_frame(frame_from)
        T2 = cls.from_frame(frame_to)
        return cls(multiply_matrices(T2.matrix, matrix_inverse(T1.matrix)))

    @classmethod
    def from_change_of_basis(cls, frame_from, frame_to):
        """Construct a change of basis transformation between two frames.

        A basis change is essentially a remapping of geometry from one
        coordinate system to another.

        Parameters
        ----------
        frame_from : :class:`~compas.geometry.Frame`
            A frame defining the original Cartesian coordinate system.
        frame_to : :class:`~compas.geometry.Frame`
            A frame defining the targeted Cartesian coordinate system.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The transformation representing a change of basis.

        Examples
        --------
        >>> from compas.geometry import Point, Frame
        >>> f1 = Frame([2, 2, 2], [0.12, 0.58, 0.81], [-0.80, 0.53, -0.26])
        >>> f2 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_change_of_basis(f1, f2)
        >>> p_f1 = Point(1, 1, 1)  # point in f1
        >>> p_f1.transformed(T)  # point represented in f2
        Point(1.395, 0.955, 1.934)
        >>> Frame.local_to_local_coordinates(f1, f2, p_f1)
        Point(1.395, 0.955, 1.934)

        """
        T1 = cls.from_frame(frame_from)
        T2 = cls.from_frame(frame_to)
        return cls(multiply_matrices(matrix_inverse(T2.matrix), T1.matrix))

    # ==========================================================================
    # methods
    # ==========================================================================

    def copy(self):
        """Returns a copy of the transformation.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            An independent copy of the transformation.

        """
        cls = type(self)
        matrix = [
            self.matrix[0][:],
            self.matrix[1][:],
            self.matrix[2][:],
            self.matrix[3][:],
        ]
        return cls(matrix)

    def transpose(self):
        """Transpose the matrix of this transformation.

        Returns
        -------
        None
            The transformation is transposed in-place.

        """
        self.matrix = transpose_matrix(self.matrix)

    def transposed(self):
        """Create a transposed copy of this transformation.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The transposed transformation object.

        """
        T = self.copy()
        T.transpose()
        return T

    def invert(self):
        """Invert this transformation.

        Returns
        -------
        None
            The transformation is transposed in-place.

        """
        self.matrix = matrix_inverse(self.matrix)

    def inverse(self):
        """Returns the inverse transformation.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The inverse transformation.

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> f = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(f)
        >>> I = Transformation(identity_matrix(4))
        >>> I == T * T.inverse()
        True

        """
        T = self.copy()
        T.invert()
        return T

    inverted = inverse

    def decomposed(self):
        """Decompose the `Transformation` into its components.

        Returns
        -------
        :class:`~compas.geometry.Scale`
            The scale component of the current transformation.
        :class:`~compas.geometry.Shear`
            The shear component of the current transformation.
        :class:`~compas.geometry.Rotation`
            The rotation component of the current transformation.
        :class:`~compas.geometry.Translation`
            The translation component of the current transformation.
        :class:`~compas.geometry.Projection`
            The projection component of the current transformation.

        Examples
        --------
        >>> from compas.geometry import Scale, Translation, Rotation
        >>> trans1 = [1, 2, 3]
        >>> angle1 = [-2.142, 1.141, -0.142]
        >>> scale1 = [0.123, 2, 0.5]
        >>> T1 = Translation.from_vector(trans1)
        >>> R1 = Rotation.from_euler_angles(angle1)
        >>> S1 = Scale.from_factors(scale1)
        >>> M = T1 * R1 * S1
        >>> S, H, R, T, P = M.decomposed()
        >>> S1 == S
        True
        >>> R1 == R
        True
        >>> T1 == T
        True

        """
        from compas.geometry import Scale  # noqa: F811
        from compas.geometry import Shear
        from compas.geometry import Rotation  # noqa: F811
        from compas.geometry import Translation  # noqa: F811
        from compas.geometry import Projection

        s, h, a, t, p = decompose_matrix(self.matrix)
        S = Scale.from_factors(s)
        H = Shear.from_entries(h)
        R = Rotation.from_euler_angles(a, static=True, axes="xyz")
        T = Translation.from_vector(t)
        P = Projection.from_entries(p)
        return S, H, R, T, P

    def concatenate(self, other):
        """Concatenate another transformation to this transformation.

        Parameters
        ----------
        other: :class:`~compas.geometry.Transformation`
            The transformation object to concatenate.

        Returns
        -------
        None
            This transformation object is changed in-place.

        Notes
        -----
        Rz * Ry * Rx means that Rx is first transformation, Ry second, and Rz third.

        """
        self.matrix = multiply_matrices(self.matrix, other.matrix)

    def concatenated(self, other):
        """Concatenate two transformations into one `Transformation`.

        Parameters
        ----------
        other : :class:`~compas.geometry.Transformation`
            The transformation object to concatenate.

        Returns
        -------
        :class:`~compas.geometry.Transformation`
            The new transformation that is the concatenation of this one and the other.

        Notes
        -----
        Rz * Ry * Rx means that Rx is first transformation, Ry second, and Rz third.

        """
        cls = type(self)
        if isinstance(other, cls):
            return cls(multiply_matrices(self.matrix, other.matrix))
        return Transformation(multiply_matrices(self.matrix, other.matrix))


# src/compas/geometry/translation.py

"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""
from compas.utilities import flatten
from compas.geometry import allclose
from compas.geometry import matrix_from_translation
from compas.geometry import translation_from_matrix
from compas.geometry import Transformation


class Translation(Transformation):
    """Class representing a translation transformation.

    Parameters
    ----------
    matrix : list[list[float]], optional
        A 4x4 matrix (or similar) representing a translation.
    check : bool, optional
        If ``True``, the provided matrix will be checked for validity.

    Attributes
    ----------
    translation_vector : :class:`~compas.geometry.Vector`
        The translation vector.

    Raises
    ------
    ValueError
        If ``check`` is ``True`` and the provided transformation matrix is not a translation.

    Examples
    --------
    >>> T = Translation.from_vector([1, 2, 3])
    >>> T[0, 3] == 1
    True
    >>> T[1, 3] == 2
    True
    >>> T[2, 3] == 3
    True

    >>> from compas.geometry import Vector
    >>> T = Translation.from_vector(Vector(1, 2, 3))
    >>> T[0, 3] == 1
    True
    >>> T[1, 3] == 2
    True
    >>> T[2, 3] == 3
    True

    >>> T = Translation([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]])
    >>> T[0, 3] == 1
    True
    >>> T[1, 3] == 2
    True
    >>> T[2, 3] == 3
    True

    """

    def __init__(self, matrix=None, check=False):
        if matrix and check:
            translation = translation_from_matrix(matrix)
            if not allclose(flatten(matrix), flatten(matrix_from_translation(translation))):
                raise ValueError("This is not a proper translation matrix.")
        super(Translation, self).__init__(matrix=matrix)

    @property
    def translation_vector(self):
        from compas.geometry import Vector

        x = self.matrix[0][3]
        y = self.matrix[1][3]
        z = self.matrix[2][3]
        return Vector(x, y, z)

    def __repr__(self):
        return "Translation({0!r}, check=False)".format(self.matrix)

    @classmethod
    def from_vector(cls, vector):
        """Create a translation transformation from a translation vector.

        Parameters
        ----------
        vector : [float, float, float] | :class:`~compas.geometry.Vector`
            The translation vector.

        Returns
        -------
        :class:`~compas.geometry.Translation`
            The translation transformation.

        """
        matrix = matrix_from_translation(vector)
        return cls(matrix)


# src/compas/geometry/vector.py

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

from compas import PRECISION

from compas.geometry import length_vector
from compas.geometry import cross_vectors
from compas.geometry import subtract_vectors
from compas.geometry import dot_vectors
from compas.geometry import angle_vectors
from compas.geometry import angle_vectors_signed
from compas.geometry import angles_vectors
from compas.geometry import transform_vectors
from compas.geometry import Geometry


class Vector(Geometry):
    """A vector is defined by XYZ components and a homogenisation factor.

    Parameters
    ----------
    x : float
        The X component of the vector.
    y : float
        The Y component of the vector.
    z : float
        The Z component of the vector.

    Attributes
    ----------
    x : float
        The X coordinate of the point.
    y : float
        The Y coordinate of the point.
    z : float
        The Z coordinate of the point.
    length : float, read-only
        The length of this vector.

    Examples
    --------
    >>> u = Vector(1, 0, 0)
    >>> v = Vector(0, 1, 0)
    >>> u
    Vector(1.000, 0.000, 0.000)
    >>> v
    Vector(0.000, 1.000, 0.000)
    >>> u.x
    1.0
    >>> u[0]
    1.0
    >>> u.length
    1.0
    >>> u + v
    Vector(1.000, 1.000, 0.000)
    >>> u + [0.0, 1.0, 0.0]
    Vector(1.000, 1.000, 0.000)
    >>> u * 2
    Vector(2.000, 0.000, 0.000)
    >>> u.dot(v)
    0.0
    >>> u.cross(v)
    Vector(0.000, 0.000, 1.000)

    """

    JSONSCHEMA = {
        "type": "array",
        "minItems": 3,
        "maxItems": 3,
        "items": {"type": "number"},
    }

    def __init__(self, x, y, z=0.0, **kwargs):
        super(Vector, self).__init__(**kwargs)
        self._x = 0.0
        self._y = 0.0
        self._z = 0.0
        self.x = x
        self.y = y
        self.z = z

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        """dict : The data dictionary that represents the vector."""
        return list(self)

    @data.setter
    def data(self, data):
        self.x = data[0]
        self.y = data[1]
        self.z = data[2]

    @classmethod
    def from_data(cls, data):
        """Construct a vector from a data dict.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The vector constructed from the provided data.

        Examples
        --------
        >>> Vector.from_data([0.0, 0.0, 1.0])
        Vector(0.000, 0.000, 1.000)
        """
        return cls(*data)

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, x):
        self._x = float(x)

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, y):
        self._y = float(y)

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self, z):
        self._z = float(z)

    @property
    def length(self):
        return length_vector(self)

    # ==========================================================================
    # customization
    # ==========================================================================

    def __repr__(self):
        return "Vector({0:.{3}f}, {1:.{3}f}, {2:.{3}f})".format(self.x, self.y, self.z, PRECISION[:1])

    def __len__(self):
        return 3

    def __getitem__(self, key):
        if isinstance(key, slice):
            return [self[i] for i in range(*key.indices(len(self)))]
        i = key % 3
        if i == 0:
            return self.x
        if i == 1:
            return self.y
        if i == 2:
            return self.z
        raise KeyError

    def __setitem__(self, key, value):
        i = key % 3
        if i == 0:
            self.x = value
            return
        if i == 1:
            self.y = value
            return
        if i == 2:
            self.z = value
            return
        raise KeyError

    def __iter__(self):
        return iter([self.x, self.y, self.z])

    def __eq__(self, other):
        """Is this vector equal to the other vector?

        Two vectors are considered equal if their XYZ components are identical.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The vector to compare.

        Returns
        -------
        bool
            True if the vectors are equal.
            False otherwise.

        """
        return self.x == other[0] and self.y == other[1] and self.z == other[2]

    def __add__(self, other):
        """Return a vector that is the the sum of this vector and another vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The vector to add.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The resulting vector.

        """
        return Vector(self.x + other[0], self.y + other[1], self.z + other[2])

    def __sub__(self, other):
        """Return a vector that is the the difference between this vector and another vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The vector to subtract.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The resulting new vector.

        """
        return Vector(self.x - other[0], self.y - other[1], self.z - other[2])

    def __mul__(self, n):
        """Return a vector that is the scaled version of this vector.

        Parameters
        ----------
        n : float
            The scaling factor.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The resulting new vector.

        """
        return Vector(self.x * n, self.y * n, self.z * n)

    def __truediv__(self, n):
        """Return a vector that is the scaled version of this vector.

        Parameters
        ----------
        n : float
            The scaling factor.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The resulting new vector.

        """
        return Vector(self.x / n, self.y / n, self.z / n)

    def __pow__(self, n):
        """Create a vector from the components of the current vector raised
        to the given power.

        Parameters
        ----------
        n : float
            The power.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A new point with raised coordinates.

        """
        return Vector(self.x**n, self.y**n, self.z**n)

    def __neg__(self):
        return self.scaled(-1.0)

    def __iadd__(self, other):
        """Add the components of the other vector to this vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The vector to add.

        Returns
        -------
        None

        """
        self.x += other[0]
        self.y += other[1]
        self.z += other[2]
        return self

    def __isub__(self, other):
        """Subtract the components of the other vector from this vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The vector to subtract.

        Returns
        -------
        None

        """
        self.x -= other[0]
        self.y -= other[1]
        self.z -= other[2]
        return self

    def __imul__(self, n):
        """Multiply the components of this vector by the given factor.

        Parameters
        ----------
        n : float
            The multiplication factor.

        Returns
        -------
        None

        """
        self.x *= n
        self.y *= n
        self.z *= n
        return self

    def __itruediv__(self, n):
        """Divide the components of this vector by the given factor.

        Parameters
        ----------
        n : float
            The multiplication factor.

        Returns
        -------
        None

        """
        self.x /= n
        self.y /= n
        self.z /= n
        return self

    def __ipow__(self, n):
        """Raise the components of this vector to the given power.

        Parameters
        ----------
        n : float
            The power.

        Returns
        -------
        None

        """
        self.x **= n
        self.y **= n
        self.z **= n
        return self

    # ==========================================================================
    # constructors
    # ==========================================================================

    @classmethod
    def Xaxis(cls):
        """Construct a unit vector along the X axis.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A vector with components ``x = 1.0, y = 0.0, z = 0.0``.

        Examples
        --------
        >>> Vector.Xaxis()
        Vector(1.000, 0.000, 0.000)

        """
        return cls(1.0, 0.0, 0.0)

    @classmethod
    def Yaxis(cls):
        """Construct a unit vector along the Y axis.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A vector with components ``x = 0.0, y = 1.0, z = 0.0``.

        Examples
        --------
        >>> Vector.Yaxis()
        Vector(0.000, 1.000, 0.000)

        """
        return cls(0.0, 1.0, 0.0)

    @classmethod
    def Zaxis(cls):
        """Construct a unit vector along the Z axis.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A vector with components ``x = 0.0, y = 0.0, z = 1.0``.

        Examples
        --------
        >>> Vector.Zaxis()
        Vector(0.000, 0.000, 1.000)

        """
        return cls(0.0, 0.0, 1.0)

    @classmethod
    def from_start_end(cls, start, end):
        """Construct a vector from start and end points.

        Parameters
        ----------
        start : [float, float, float] | :class:`~compas.geometry.Point`
            The start point.
        end : [float, float, float] | :class:`~compas.geometry.Point`
            The end point.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The vector from start to end.

        Examples
        --------
        >>> Vector.from_start_end([1.0, 0.0, 0.0], [1.0, 1.0, 0.0])
        Vector(0.000, 1.000, 0.000)

        """
        v = subtract_vectors(end, start)
        return cls(*v)

    # ==========================================================================
    # static
    # ==========================================================================

    @staticmethod
    def transform_collection(collection, X):
        """Transform a collection of vector objects.

        Parameters
        ----------
        collection : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            The collection of vectors.

        Returns
        -------
        None

        Examples
        --------
        >>> from compas.geometry import Rotation
        >>> R = Rotation.from_axis_and_angle(Vector.Zaxis(), math.radians(90))
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> vectors = [u]
        >>> Vector.transform_collection(vectors, R)
        >>> v = vectors[0]
        >>> v
        Vector(0.000, 1.000, 0.000)
        >>> u is v
        True

        """
        data = transform_vectors(collection, X)
        for vector, xyz in zip(collection, data):
            vector.x = xyz[0]
            vector.y = xyz[1]
            vector.z = xyz[2]

    @staticmethod
    def transformed_collection(collection, X):
        """Create a collection of transformed vectors.

        Parameters
        ----------
        collection : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            The collection of vectors.

        Returns
        -------
        list[:class:`~compas.geometry.Vector`]
            The transformed vectors.

        Examples
        --------
        >>> from compas.geometry import Rotation
        >>> R = Rotation.from_axis_and_angle(Vector.Zaxis(), math.radians(90))
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> vectors = [u]
        >>> vectors = Vector.transformed_collection(vectors, R)
        >>> v = vectors[0]
        >>> v
        Vector(0.000, 1.000, 0.000)
        >>> u is v
        False

        """
        vectors = [vector.copy() for vector in collection]
        Vector.transform_collection(vectors, X)
        return vectors

    @staticmethod
    def length_vectors(vectors):
        """Compute the length of multiple vectors.

        Parameters
        ----------
        vectors : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.

        Returns
        -------
        list[float]
            A list of lengths.

        Examples
        --------
        >>> Vector.length_vectors([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]])
        [1.0, 2.0]

        """
        return [length_vector(vector) for vector in vectors]

    @staticmethod
    def sum_vectors(vectors):
        """Compute the sum of multiple vectors.

        Parameters
        ----------
        vectors : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A vector that is the sum of the vectors.

        Examples
        --------
        >>> Vector.sum_vectors([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]])
        Vector(3.000, 0.000, 0.000)

        """
        return Vector(*[sum(axis) for axis in zip(*vectors)])

    @staticmethod
    def dot_vectors(left, right):
        """Compute the dot product of two lists of vectors.

        Parameters
        ----------
        left : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.
        right : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.

        Returns
        -------
        list[float]
            A list of dot products.

        Examples
        --------
        >>> Vector.dot_vectors([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]], [[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]])
        [1.0, 4.0]

        """
        return [Vector.dot(u, v) for u, v in zip(left, right)]

    @staticmethod
    def cross_vectors(left, right):
        """Compute the cross product of two lists of vectors.

        Parameters
        ----------
        left : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.
        right : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.

        Returns
        -------
        list[:class:`~compas.geometry.Vector`]
            A list of cross products.

        Examples
        --------
        >>> Vector.cross_vectors([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]], [[0.0, 1.0, 0.0], [0.0, 0.0, 2.0]])
        [Vector(0.000, 0.000, 1.000), Vector(0.000, -4.000, 0.000)]

        """
        # cross_vectors(u,v) from src\compas\geometry\_core\_algebra.py
        return [Vector(*cross_vectors(u, v)) for u, v in zip(left, right)]

    @staticmethod
    def angles_vectors(left, right):
        """Compute both angles between corresponding pairs of two lists of vectors.

        Parameters
        ----------
        left : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.
        right : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.

        Returns
        -------
        list[float]
            A list of angle pairs.

        Examples
        --------
        >>> Vector.angles_vectors([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]], [[0.0, 1.0, 0.0], [0.0, 0.0, 2.0]])
        [(1.5707963267948966, 4.71238898038469), (1.5707963267948966, 4.71238898038469)]

        """
        return [angles_vectors(u, v) for u, v in zip(left, right)]

    @staticmethod
    def angle_vectors(left, right):
        """Compute the smallest angle between corresponding pairs of two lists of vectors.

        Parameters
        ----------
        left : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.
        right : list[[float, float, float] | :class:`~compas.geometry.Vector`]
            A list of vectors.

        Returns
        -------
        list[float]
            A list of angles.

        Examples
        --------
        >>> Vector.angle_vectors([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]], [[0.0, 1.0, 0.0], [0.0, 0.0, 2.0]])
        [1.5707963267948966, 1.5707963267948966]

        """
        return [angle_vectors(u, v) for u, v in zip(left, right)]

    # ==========================================================================
    # helpers
    # ==========================================================================

    def copy(self):
        """Make a copy of this vector.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The copy.

        Examples
        --------
        >>> u = Vector(0.0, 0.0, 0.0)
        >>> v = u.copy()
        >>> u == v
        True
        >>> u is v
        False

        """
        cls = type(self)
        return cls(self.x, self.y, self.z)

    # ==========================================================================
    # methods
    # ==========================================================================

    def unitize(self):
        """Scale this vector to unit length.

        Returns
        -------
        None

        Examples
        --------
        >>> u = Vector(1.0, 2.0, 3.0)
        >>> u.unitize()
        >>> u.length
        1.0

        """
        length = self.length
        self.x = self.x / length
        self.y = self.y / length
        self.z = self.z / length

    def unitized(self):
        """Returns a unitized copy of this vector.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A unitized copy of the vector.

        Examples
        --------
        >>> u = Vector(1.0, 2.0, 3.0)
        >>> v = u.unitized()
        >>> u.length == 1.0
        False
        >>> v.length == 1.0
        True

        """
        v = self.copy()
        v.unitize()
        return v

    def invert(self):
        """Invert the direction of this vector

        Returns
        -------
        None

        Notes
        -----
        a negation of a vector is similar to inverting a vector

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = u.copy()
        >>> u.invert()
        >>> u == v
        False
        >>> u.invert()
        >>> u == v
        True
        >>> v == --v
        True

        """
        self.scale(-1.0)

    def inverted(self):
        """Returns a inverted copy of this vector

        Returns
        -------
        :class:`~compas.geometry.Vector`

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = u.inverted()
        >>> w = u + v
        >>> w.length
        0.0

        """
        return self.scaled(-1.0)

    def scale(self, n):
        """Scale this vector by a factor n.

        Parameters
        ----------
        n : float
            The scaling factor.

        Returns
        -------
        None

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> u.scale(3.0)
        >>> u.length
        3.0

        """
        self.x *= n
        self.y *= n
        self.z *= n

    def scaled(self, n):
        """Returns a scaled copy of this vector.

        Parameters
        ----------
        n : float
            The scaling factor.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            A scaled copy of the vector.

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = u.scaled(3.0)
        >>> u.length
        1.0
        >>> v.length
        3.0

        """
        v = self.copy()
        v.scale(n)
        return v

    def dot(self, other):
        """The dot product of this vector and another vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The other vector.

        Returns
        -------
        float
            The dot product.

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = Vector(0.0, 1.0, 0.0)
        >>> u.dot(v)
        0.0

        """
        return dot_vectors(self, other)

    def cross(self, other):
        """The cross product of this vector and another vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The other vector.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The cross product.

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = Vector(0.0, 1.0, 0.0)
        >>> u.cross(v)
        Vector(0.000, 0.000, 1.000)

        """
        return Vector(*cross_vectors(self, other))

    def angle(self, other):
        """Compute the smallest angle between this vector and another vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The other vector.

        Returns
        -------
        float
            The smallest angle between the two vectors.

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = Vector(0.0, 1.0, 0.0)
        >>> u.angle(v) == 0.5 * math.pi
        True

        """
        return angle_vectors(self, other)

    def angle_signed(self, other, normal):
        """Compute the signed angle between this vector and another vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The other vector.
        normal : [float, float, float] | :class:`~compas.geometry.Vector`
            The plane's normal spanned by this and the other vector.

        Returns
        -------
        float
            The signed angle between the two vectors.

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = Vector(0.0, 1.0, 0.0)
        >>> u.angle_signed(v, Vector(0.0, 0.0, 1.0)) == 0.5 * math.pi
        True
        >>> u.angle_signed(v, Vector(0.0, 0.0, -1.0)) == -0.5 * math.pi
        True

        """
        return angle_vectors_signed(self, other, normal)

    def angles(self, other):
        """Compute both angles between this vector and another vector.

        Parameters
        ----------
        other : [float, float, float] | :class:`~compas.geometry.Vector`
            The other vector.

        Returns
        -------
        tuple[float, float]
            The angles between the two vectors, with the smallest angle first.

        Examples
        --------
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> v = Vector(0.0, 1.0, 0.0)
        >>> u.angles(v)[0] == 0.5 * math.pi
        True

        """
        return angles_vectors(self, other)

    def transform(self, T):
        """Transform this vector.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation` | list[list[float]]
            The transformation.

        Returns
        -------
        None

        Examples
        --------
        >>> from compas.geometry import Rotation
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> R = Rotation.from_axis_and_angle([0.0, 0.0, 1.0], math.radians(90))
        >>> u.transform(R)
        >>> u
        Vector(0.000, 1.000, 0.000)

        """
        point = transform_vectors([self], T)[0]
        self.x = point[0]
        self.y = point[1]
        self.z = point[2]

    def transformed(self, T):
        """Return a transformed copy of this vector.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation` | list[list[float]]
            The transformation.

        Returns
        -------
        :class:`~compas.geometry.Vector`
            The transformed copy.

        Examples
        --------
        >>> from compas.geometry import Rotation
        >>> u = Vector(1.0, 0.0, 0.0)
        >>> R = Rotation.from_axis_and_angle([0.0, 0.0, 1.0], math.radians(90))
        >>> v = u.transformed(R)
        >>> v
        Vector(0.000, 1.000, 0.000)

        """
        vector = self.copy()
        vector.transform(T)
        return vector


# src/compas/geometry/surfaces/nurbs.py

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

from compas.plugins import pluggable
from compas.geometry import Point
from compas.utilities import linspace
from compas.utilities import meshgrid

from .surface import Surface


@pluggable(category="factories")
def new_nurbssurface(cls, *args, **kwargs):
    raise NotImplementedError


@pluggable(category="factories")
def new_nurbssurface_from_parameters(cls, *args, **kwargs):
    raise NotImplementedError


@pluggable(category="factories")
def new_nurbssurface_from_points(cls, *args, **kwargs):
    raise NotImplementedError


@pluggable(category="factories")
def new_nurbssurface_from_fill(cls, *args, **kwargs):
    raise NotImplementedError


@pluggable(category="factories")
def new_nurbssurface_from_step(cls, *args, **kwargs):
    raise NotImplementedError


class NurbsSurface(Surface):
    """A NURBS surface is defined by control points, weights, knots, and a degree, in two directions U and V.

    Parameters
    ----------
    name : str, optional
        The name of the surface.

    Attributes
    ----------
    points : list[list[:class:`~compas.geometry.Point`]], read-only
        The control points as rows along the U direction.
    weights : list[list[float]], read-only
        The weights of the control points.
    u_knots : list[float], read-only
        The knots in the U direction, without multiplicity.
    v_knots : list[float], read-only
        The knots in the V direction, without multiplicity.
    u_mults : list[int], read-only
        Multiplicity of the knots in the U direction.
    v_mults : list[int], read-only
        Multiplicity of the knots in the V direction.
    u_degree : list[int], read-only
        The degree of the surface in the U direction.
    v_degree : list[int], read-only
        The degree of the surface in the V direction.

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "points": {"type": "array", "items": {"type": "array", "items": Point.JSONSCHEMA}},
            "weights": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}},
            "u_knots": {"type": "array", "items": {"type": "number"}},
            "v_knots": {"type": "array", "items": {"type": "number"}},
            "u_mults": {"type": "array", "items": {"type": "integer"}},
            "v_mults": {"type": "array", "items": {"type": "integer"}},
            "u_degree": {"type": "integer", "exclusiveMinimum": 0},
            "v_degree": {"type": "integer", "exclusiveMinimum": 0},
            "is_u_periodic": {"type": "boolean"},
            "is_v_periodic": {"type": "boolean"},
        },
        "additionalProperties": False,
        "minProperties": 10,
    }

    def __new__(cls, *args, **kwargs):
        return new_nurbssurface(cls, *args, **kwargs)

    def __init__(self, name=None):
        super(NurbsSurface, self).__init__(name=name)

    def __str__(self):
        lines = [
            "NurbsSurface",
            "------------",
            "Points: {}".format(self.points),
            "Weights: {}".format(self.weights),
            "U Knots: {}".format(self.u_knots),
            "V Knots: {}".format(self.v_knots),
            "U Mults: {}".format(self.u_mults),
            "V Mults: {}".format(self.v_mults),
            "U Degree: {}".format(self.u_degree),
            "V Degree: {}".format(self.v_degree),
            "U Domain: {}".format(self.u_domain),
            "V Domain: {}".format(self.v_domain),
            "U Periodic: {}".format(self.is_u_periodic),
            "V Periodic: {}".format(self.is_v_periodic),
        ]
        return "\n".join(lines)

    # ==============================================================================
    # Data
    # ==============================================================================

    @property
    def dtype(self):
        return "compas.geometry/NurbsSurface"

    @property
    def data(self):
        """dict : Representation of the curve as a dict containing only native Python objects."""
        return {
            "points": self.points,
            "weights": self.weights,
            "u_knots": self.u_knots,
            "v_knots": self.v_knots,
            "u_mults": self.u_mults,
            "v_mults": self.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):
        raise NotImplementedError

    @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 = 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,
        )

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def points(self):
        raise NotImplementedError

    @property
    def weights(self):
        raise NotImplementedError

    @property
    def u_knots(self):
        raise NotImplementedError

    @property
    def v_knots(self):
        raise NotImplementedError

    @property
    def u_mults(self):
        raise NotImplementedError

    @property
    def v_mults(self):
        raise NotImplementedError

    @property
    def u_degree(self):
        raise NotImplementedError

    @property
    def v_degree(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @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[[float, float, float] | :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`

        """
        return new_nurbssurface_from_parameters(
            cls,
            points,
            weights,
            u_knots,
            v_knots,
            u_mults,
            v_mults,
            u_degree,
            v_degree,
            is_u_periodic=is_u_periodic,
            is_v_periodic=is_v_periodic,
        )

    @classmethod
    def from_points(cls, points, u_degree=3, v_degree=3):
        """Construct a NURBS surface from control points.

        Parameters
        ----------
        points : list[list[[float, float, float] | :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`

        """
        return new_nurbssurface_from_points(cls, points, u_degree=u_degree, v_degree=v_degree)

    @classmethod
    def from_meshgrid(cls, nu=10, nv=10):
        """Construct a NURBS surface from a mesh grid.

        Parameters
        ----------
        nu : int, optional
            Number of control points in the U direction.
        nv : int, optional
            Number of control points in the V direction.

        Returns
        -------
        :class:`~compas.geometry.NurbsSurface`

        """
        UU, VV = meshgrid(linspace(0, nu, nu + 1), linspace(0, nv, nv + 1))
        points = []
        for U, V in zip(UU, VV):
            row = []
            for u, v in zip(U, V):
                row.append(Point(u, v, 0.0))
            points.append(row)
        return cls.from_points(points=points)

    @classmethod
    def from_step(cls, filepath):
        """Load a NURBS surface from a STP file.

        Parameters
        ----------
        filepath : str

        Returns
        -------
        :class:`~compas.geometry.NurbsSurface`

        """
        return new_nurbssurface_from_step(cls, filepath)

    @classmethod
    def from_fill(cls, curve1, curve2, curve3=None, curve4=None, style="stretch"):
        """Construct a NURBS surface from the infill between two, three or four contiguous NURBS curves.

        Parameters
        ----------
        curve1 : :class:`compas.geometry.NurbsCurve`
        curve2 : :class:`compas.geometry.NurbsCurve`
        curve3 : :class:`compas.geometry.NurbsCurve`, optional.
        curve4 : :class:`compas.geometry.NurbsCurve`, optional.
        style : Literal['stretch', 'coons', 'curved'], optional.

            * ``'stretch'`` produces the flattest patch.
            * ``'curved'`` produces a rounded patch.
            * ``'coons'`` is between stretch and coons.

        Raises
        ------
        ValueError
            If the fill style is not supported.

        Returns
        -------
        :class:`~compas.geometry.NurbsSurface`

        """
        return new_nurbssurface_from_fill(cls, curve1, curve2, curve3, curve4, style)

    # ==============================================================================
    # Conversions
    # ==============================================================================

    # ==============================================================================
    # Methods
    # ==============================================================================

    def copy(self):
        """Make an independent copy of the surface.

        Returns
        -------
        :class:`~compas.geometry.NurbsSurface`

        """
        return NurbsSurface.from_parameters(
            self.points,
            self.weights,
            self.u_knots,
            self.v_knots,
            self.u_mults,
            self.v_mults,
            self.u_degree,
            self.v_degree,
            self.is_u_periodic,
            self.is_v_periodic,
        )


# src/compas/geometry/surfaces/planar.py

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

from compas.geometry import Frame
from .surface import Surface


class PlanarSurface(Surface):
    """A planar surface is defined by a local coordinate system and a size.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame`, optional
        The local coordinate system of the surface.
        Default is ``None``, in which case the world coordinate system is used.
    xsize : float, optional
        The size of the surface in the local X-direction.
    ysize : float, optional
        The size of the surface in the local Y-direction.

    Attributes
    ----------

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "xsize": {"type": "number", "minimum": 0},
            "ysize": {"type": "number", "minimum": 0},
            "frame": Frame.JSONSCHEMA,
        },
        "required": ["xsize", "ysize", "frame"],
    }

    def __init__(self, xsize=1.0, ysize=1.0, frame=None):
        super(PlanarSurface, self).__init__(frame=frame)
        self._xsize = None
        self._ysize = None
        self.xsize = xsize
        self.ysize = ysize

    def __repr__(self):
        return "PlanarSurface(xsize={0!r}, ysize={1!r}, frame={2!r})".format(self.xsize, self.ysize, self.frame)

    def __eq__(self, other):
        try:
            other_frame = other.frame
            other_xsize = other.xsize
            other_ysize = other.ysize
        except Exception:
            return False
        return self.xsize == other_xsize and self.ysize == other_ysize and self.frame == other_frame

    @property
    def data(self):
        return {"xsize": self.xsize, "ysize": self.ysize, "frame": self.frame}

    @data.setter
    def data(self, data):
        self.frame = data["frame"]
        self.xsize = data["xsize"]
        self.ysize = data["ysize"]

    @property
    def xsize(self):
        if not self._xsize:
            raise ValueError("The size of the surface in the local X-direction is not set.")
        return self._xsize

    @xsize.setter
    def xsize(self, xsize):
        if xsize <= 0:
            raise ValueError("The size of the surface in the local X-direction should be larger than zero.")
        self._xsize = float(xsize)

    @property
    def ysize(self):
        if not self._ysize:
            raise ValueError("The size of the surface in the local Y-direction is not set.")
        return self._ysize

    @ysize.setter
    def ysize(self, ysize):
        if ysize <= 0:
            raise ValueError("The size of the surface in the local Y-direction should be larger than zero.")
        self._ysize = float(ysize)

    # =============================================================================
    # Constructors
    # =============================================================================

    @classmethod
    def from_plane_and_size(cls, plane, xsize, ysize):
        """Construct a sphere from a plane and x and y sizes.

        Parameters
        ----------
        plane : :class:`compas.geometry.Plane`
            The plane of the sphere.
        xsize : float
            The size of the sphere in the local X-direction.
        ysize : float
            The size of the sphere in the local Y-direction.

        Returns
        -------
        :class:`compas.geometry.PlanarSurface`
            A planar surface.

        """
        return cls(xsize, ysize, frame=Frame.from_plane(plane))

    # =============================================================================
    # Conversions
    # =============================================================================

    # =============================================================================
    # Methods
    # =============================================================================

    def point_at(self, u, v):
        """Construct a point on the sphere.

        Parameters
        ----------
        u : float
            The first parameter.
        v : float
            The second parameter.

        Returns
        -------
        :class:`compas.geometry.Point`
            A point on the sphere.

        """
        return self.frame.point + self.frame.xaxis * u + self.frame.yaxis * v


# src/compas/geometry/surfaces/spherical.py

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

from math import cos, sin, pi

from compas.geometry import Frame
from compas.geometry import Circle
from .surface import Surface

PI2 = 2 * pi


class SphericalSurface(Surface):
    """A spherical surface is defined by a radius and a frame.

    Parameters
    ----------
    radius : float
        The radius of the sphere.
    frame : :class:`Frame`
        The frame of the sphere.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> from compas.geometry import SphericalSurface
    >>> frame = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0])
    >>> sphere = SphericalSurface(1.0, frame)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "radius": {"type": "number", "minimum": 0},
            "frame": Frame.JSONSCHEMA,
        },
        "required": ["radius", "frame"],
    }

    # overwriting the __new__ method is necessary
    # to avoid triggering the plugin mechanism of the base surface class
    def __new__(cls, *args, **kwargs):
        surface = object.__new__(cls)
        surface.__init__(*args, **kwargs)
        return surface

    def __init__(self, radius, frame=None, **kwargs):
        super(SphericalSurface, self).__init__(frame=frame, **kwargs)
        self._radius = None
        self.radius = radius

    def __repr__(self):
        return "SphericalSurface(radius={0!r}, frame={1!r})".format(self.radius, self.frame)

    def __eq__(self, other):
        try:
            other_frame = other.frame
            other_radius = other.radius
        except Exception:
            return False
        return self.radius == other_radius and self.frame == other_frame

    @property
    def data(self):
        return {"radius": self.radius, "frame": self.frame}

    @data.setter
    def data(self, data):
        self.radius = data["radius"]
        self.frame = data["frame"]

    @property
    def center(self):
        return self.frame.point

    @center.setter
    def center(self, point):
        self.frame.point = point

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("The radius of the surface has not been set yet.")
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("The radius of a sphere should be larger than or equal to zero.")
        self._radius = float(radius)

    @property
    def area(self):
        return 4 * 3.14159 * self.radius**2

    @property
    def volume(self):
        return 4 / 3 * 3.14159 * self.radius**3

    # =============================================================================
    # Constructors
    # =============================================================================

    @classmethod
    def from_plane_and_radius(cls, plane, radius):
        """Construct a sphere from a plane and a radius.

        Parameters
        ----------
        plane : :class:`compas.geometry.Plane`
            The plane of the sphere.
        radius : float
            The radius of the sphere.

        Returns
        -------
        :class:`compas.geometry.SphericalSurface`
            A sphere.

        """
        return cls(radius, frame=Frame.from_plane(plane))

    @classmethod
    def from_three_points(cls, a, b, c):
        """Construct a sphere from three points.

        Parameters
        ----------
        a : :class:`compas.geometry.Point`
            The first point.
        b : :class:`compas.geometry.Point`
            The second point.
        c : :class:`compas.geometry.Point`
            The third point.

        Returns
        -------
        :class:`compas.geometry.SphericalSurface`
            A sphere.

        """
        circle = Circle.from_three_points(a, b, c)
        return cls(circle.radius, frame=circle.frame)

    @classmethod
    def from_points(cls, points):
        """Construct the sphere that best fits a set of points in the least squares sense.

        Parameters
        ----------
        points : list of :class:`compas.geometry.Point`
            The points.

        Returns
        -------
        :class:`compas.geometry.SphericalSurface`
            A sphere.

        """
        if len(points) < 3:
            raise ValueError("At least three points are required to construct a sphere.")

        if len(points) == 3:
            return cls.from_three_points(*points)

        from compas.geometry import bestfit_sphere_numpy

        center, radius = bestfit_sphere_numpy(points)
        return cls(radius, frame=Frame(center, [1, 0, 0], [0, 1, 0]))

    # =============================================================================
    # Conversions
    # =============================================================================

    # =============================================================================
    # Methods
    # =============================================================================

    def point_at(self, u, v):
        """Construct a point on the sphere.

        Parameters
        ----------
        u : float
            The first parameter.
            The parameter value should be between zero and one,
            and will be mapped to the corresponding angle between zero and pi.
        v : float
            The second parameter.
            The parameter value should be between zero and one,
            and will be mapped to the corresponding angle between zero and 2 * pi.

        Returns
        -------
        :class:`compas.geometry.Point`
            A point on the sphere.

        """
        u = u * pi
        v = v * PI2
        x = self.radius * cos(u) * sin(v)
        y = self.radius * sin(u) * sin(v)
        z = self.radius * cos(v)
        return self.frame.point + self.frame.xaxis * x + self.frame.yaxis * y + self.frame.zaxis * z

    def normal_at(self, u, v):
        """Construct a normal vector at a point on the sphere.

        Parameters
        ----------
        u : float
            The first parameter.
        v : float
            The second parameter.

        Returns
        -------
        :class:`compas.geometry.Vector`
            The normal vector.

        """
        u = u * pi
        v = v * PI2
        x = cos(u) * sin(v)
        y = sin(u) * sin(v)
        z = cos(v)
        return self.frame.xaxis * x + self.frame.yaxis * y + self.frame.zaxis * z


# src/compas/geometry/surfaces/surface.py

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

from itertools import product

from compas.geometry import Geometry
from compas.geometry import Frame
from compas.geometry import Transformation
from compas.geometry import Point
from compas.plugins import pluggable
from compas.utilities import linspace


@pluggable(category="factories")
def new_surface(cls, *args, **kwargs):
    surface = object.__new__(cls)
    surface.__init__(*args, **kwargs)
    return surface


@pluggable(category="factories")
def new_surface_from_plane(cls, *args, **kwargs):
    raise NotImplementedError


class Surface(Geometry):
    """Class representing a general surface object.

    Parameters
    ----------
    name : str, optional
        The name of the surface.

    Attributes
    ----------
    frame : :class:`~compas.geometry.Frame`
        The local coordinate system of the surface.
        Default is the world coordinate system.
    transformation : :class:`~compas.geometry.Transformation`, read-only
        The transformation from the surface's local coordinate system to the world coordinate system.
    u_domain : tuple[float, float], read-only
        The parameter domain of the surface in the U direction.
    v_domain : tuple[float, float], read-only
        The parameter domain of the surface in the V direction.
    is_u_periodic : bool, read-only
        Flag indicating if the surface is periodic in the U direction.
    is_v_periodic : bool, read-only
        Flag indicating if the surface is periodic in the V direction.

    """

    def __new__(cls, *args, **kwargs):
        return new_surface(cls, *args, **kwargs)

    def __init__(self, frame=None, u_domain=None, v_domain=None, name=None):
        super(Surface, self).__init__(name=name)
        self._frame = None
        self._transformation = None
        self._u_domain = None
        self._v_domain = None
        self._point = None
        if frame:
            self.frame = frame
        self.u_domain = u_domain or (0.0, 1.0)
        self.v_domain = v_domain or (0.0, 1.0)

    def __eq__(self, other):
        raise NotImplementedError

    def __str__(self):
        return "<Surface with parameter domain U: {} and V: {}>".format(self.u_domain, self.v_domain)

    # ==============================================================================
    # Data
    # ==============================================================================

    @property
    def data(self):
        raise NotImplementedError

    @data.setter
    def data(self, data):
        raise NotImplementedError

    @classmethod
    def from_data(cls, data):
        """Construct a surface from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Surface`
            The constructed surface.

        """
        return cls(**data)

    # ==============================================================================
    # Properties
    # ==============================================================================

    @property
    def frame(self):
        if not self._frame:
            self._frame = Frame.worldXY()
        return self._frame

    @frame.setter
    def frame(self, frame):
        if not frame:
            self._frame = None
        else:
            self._frame = Frame(frame[0], frame[1], frame[2])
        self._transformation = None

    @property
    def transformation(self):
        if not self._transformation:
            self._transformation = Transformation.from_frame(self.frame)
        return self._transformation

    @property
    def point(self):
        if not self._point:
            return self.frame.point
        return self._point

    @point.setter
    def point(self, point):
        self._point = Point(*point)

    @property
    def xaxis(self):
        return self.frame.xaxis

    @property
    def yaxis(self):
        return self.frame.yaxis

    @property
    def zaxis(self):
        return self.frame.zaxis

    @property
    def dimension(self):
        return 3

    @property
    def u_domain(self):
        if not self._u_domain:
            self._u_domain = (0.0, 1.0)
        return self._u_domain

    @u_domain.setter
    def u_domain(self, domain):
        if not domain:
            self._u_domain = None
            return
        a, b = domain
        self._u_domain = a, b

    @property
    def v_domain(self):
        if not self._v_domain:
            self._v_domain = (0.0, 1.0)
        return self._v_domain

    @v_domain.setter
    def v_domain(self, domain):
        if not domain:
            self._v_domain = None
            return
        a, b = domain
        self._v_domain = a, b

    @property
    def is_closed(self):
        raise NotImplementedError

    @property
    def is_u_periodic(self):
        raise NotImplementedError

    @property
    def is_v_periodic(self):
        raise NotImplementedError

    # ==============================================================================
    # Constructors
    # ==============================================================================

    @classmethod
    def from_step(cls, filepath):
        """Load a surface from a STP file.

        Parameters
        ----------
        filepath : str
            The path to the file.

        Returns
        -------
        :class:`~compas.geometry.Surface`

        """
        raise NotImplementedError

    @classmethod
    def from_obj(cls, filepath):
        """Load a surface from an OBJ file.

        Parameters
        ----------
        filepath : str
            The path to the file.

        Returns
        -------
        :class:`~compas.geometry.Surface`

        """
        raise NotImplementedError

    @classmethod
    def from_plane(cls, plane, *args, **kwargs):
        """Construct a surface from a plane.

        Parameters
        ----------
        plane : :class:`compas.geometry.Plane`
            The plane.

        Returns
        -------
        :class:`~compas.geometry.Surface`

        """
        return new_surface_from_plane(cls, plane, *args, **kwargs)

    # ==============================================================================
    # Conversions
    # ==============================================================================

    def to_step(self, filepath, schema="AP203"):
        """Write the surface geometry to a STP file.

        Parameters
        ----------
        filepath : str
        schema : str, optional

        Returns
        -------
        None

        """
        raise NotImplementedError

    def to_vertices_and_faces(self, nu=16, nv=16, du=None, dv=None):
        """Convert the surface to a list of vertices and faces.

        Parameters
        ----------
        nu : int, optional
            The number of faces in the u direction.
            Default is ``16``.
        nv : int, optional
            The number of faces in the v direction.
            Default is ``16``.
        du : tuple, optional
            The subset of the domain in the u direction.
            Default is ``None``, in which case the entire domain is used.
        dv : tuple, optional
            The subset of the domain in the v direction.
            Default is ``None``, in which case the entire domain is used.

        Returns
        -------
        vertices : list of :class:`compas.geometry.Point`
            The vertices of the surface discretisation.
        faces : list of list of int
            The faces of the surface discretisation as lists of vertex indices.

        """
        u_domain = self.u_domain
        v_domain = self.v_domain

        du = du or u_domain
        dv = dv or v_domain

        self.u_domain = du
        self.v_domain = dv

        vertices = [self.point_at(i, j) for i, j in product(self.u_space(nu + 1), self.v_space(nv + 1))]
        faces = [
            [
                i * (nv + 1) + j,
                (i + 1) * (nv + 1) + j,
                (i + 1) * (nv + 1) + j + 1,
                i * (nv + 1) + j + 1,
            ]
            for i, j in product(range(nu), range(nv))
        ]

        self.u_domain = u_domain
        self.v_domain = v_domain

        return vertices, faces

    def to_triangles(self, nu=16, nv=16, du=None, dv=None):
        """Convert the surface to a list of triangles.

        Parameters
        ----------
        nu : int, optional
            The number of faces in the u direction.
            Default is ``16``.
        nv : int, optional
            The number of faces in the v direction.
            Default is ``16``.
        du : tuple, optional
            The subset of the domain in the u direction.
            Default is ``None``, in which case the entire domain is used.
        dv : tuple, optional
            The subset of the domain in the v direction.
            Default is ``None``, in which case the entire domain is used.


        Returns
        -------
        list[list[:class:`~compas.geometry.Point`]]

        """
        vertices, faces = self.to_vertices_and_faces(nu=nu, nv=nv, du=du, dv=dv)
        triangles = []
        for a, b, c, d in faces:
            triangles.append([vertices[a], vertices[b], vertices[c]])
            triangles.append([vertices[a], vertices[c], vertices[d]])
        return triangles

    def to_quads(self, nu=16, nv=16, du=None, dv=None):
        """Convert the surface to a list of quads.

        Parameters
        ----------
        nu : int, optional
            The number of faces in the u direction.
            Default is ``16``.
        nv : int, optional
            The number of faces in the v direction.
            Default is ``16``.
        du : tuple, optional
            The subset of the domain in the u direction.
            Default is ``None``, in which case the entire domain is used.
        dv : tuple, optional
            The subset of the domain in the v direction.
            Default is ``None``, in which case the entire domain is used.


        Returns
        -------
        list[list[:class:`~compas.geometry.Point`]]

        """
        vertices, faces = self.to_vertices_and_faces(nu=nu, nv=nv, du=du, dv=dv)
        quads = []
        for a, b, c, d in faces:
            quads.append([vertices[a], vertices[b], vertices[c], vertices[d]])
        return quads

    def to_polyhedron(self, nu=16, nv=16, du=None, dv=None):
        """Convert the surface to a polyhedron.

        Parameters
        ----------
        nu : int, optional
            The number of faces in the u direction.
            Default is ``16``.
        nv : int, optional
            The number of faces in the v direction.
            Default is ``16``.
        du : tuple, optional
            The subset of the domain in the u direction.
            Default is ``None``, in which case the entire domain is used.
        dv : tuple, optional
            The subset of the domain in the v direction.
            Default is ``None``, in which case the entire domain is used.

        Returns
        -------
        :class:`compas.datastructures.Polyhedron`
            A polyhedron object.

        """
        from compas.geometry import Polyhedron

        vertices, faces = self.to_vertices_and_faces(nu=nu, nv=nv, du=du, dv=dv)
        return Polyhedron(vertices, faces)

    def to_mesh(self, nu=16, nv=16, du=None, dv=None):
        """Convert the surface to a mesh.

        Parameters
        ----------
        nu : int, optional
            The number of faces in the u direction.
            Default is ``16``.
        nv : int, optional
            The number of faces in the v direction.
            Default is ``16``.
        du : tuple, optional
            The subset of the domain in the u direction.
            Default is ``None``, in which case the entire domain is used.
        dv : tuple, optional
            The subset of the domain in the v direction.
            Default is ``None``, in which case the entire domain is used.

        Returns
        -------
        :class:`compas.datastructures.Mesh`
            A mesh object.

        """
        from compas.datastructures import Mesh

        vertices, faces = self.to_vertices_and_faces(nu=nu, nv=nv, du=du, dv=dv)
        return Mesh.from_vertices_and_faces(vertices, faces)

    def to_tesselation(self):
        """Convert the surface to a triangle mesh.

        Returns
        -------
        :class:`~compas.datastructures.Mesh`

        """
        raise NotImplementedError

    def to_brep(self):
        """Convert the surface to a BREP representation.

        Returns
        -------
        :class:`compas.geometry.Brep`
            A BREP object.

        """
        raise NotImplementedError

    # ==============================================================================
    # Transformations
    # ==============================================================================

    def transform(self, T):
        """Transform the local coordinate system of the surface.

        Parameters
        ----------
        T : :class:`~compas.geometry.Transformation`
            The transformation.

        Returns
        -------
        None
            The curve is modified in-place.

        Notes
        -----
        The transformation matrix is applied to the local coordinate system of the surface.
        Transformations are limited to (combinations of) translations and rotations.
        All other components of the transformation matrix are ignored.

        """
        # T[0, 0] = 1
        # T[1, 1] = 1
        # T[2, 2] = 1
        # T[3, 3] = 1

        # T[0, 3] = 0
        # T[1, 3] = 0
        # T[2, 3] = 0

        # T[3, 0] = 0
        # T[3, 1] = 0
        # T[3, 2] = 0

        self.frame.transform(T)

    # ==============================================================================
    # Methods
    # ==============================================================================

    def u_space(self, n=10):
        """Compute evenly spaced parameters over the surface domain in the U direction.

        Parameters
        ----------
        n : int, optional
            The number of parameters.

        Returns
        -------
        list[float]

        """
        umin, umax = self.u_domain
        return linspace(umin, umax, n)

    def v_space(self, n=10):
        """Compute evenly spaced parameters over the surface domain in the V direction.

        Parameters
        ----------
        n : int, optional
            The number of parameters.

        Returns
        -------
        list[float]

        """
        vmin, vmax = self.v_domain
        return linspace(vmin, vmax, n)

    def u_isocurve(self, u):
        """Compute the isoparametric curve at parameter u.

        Parameters
        ----------
        u : float

        Returns
        -------
        :class:`~compas.geometry.Curve`

        """
        raise NotImplementedError

    def v_isocurve(self, v):
        """Compute the isoparametric curve at parameter v.

        Parameters
        ----------
        v : float

        Returns
        -------
        :class:`~compas_occ.geometry.Curve`

        """
        raise NotImplementedError

    def boundary(self):
        """Compute the boundary curves of the surface.

        Returns
        -------
        list[:class:`~compas.geometry.Curve`]

        """
        raise NotImplementedError

    def pointgrid(self, nu=10, nv=10):
        """Compute point locations corresponding to evenly spaced parameters over the surface domain.

        Parameters
        ----------
        nu : int, optional
            The size of the grid in the U direction.
        nv : int, optional
            The size of the grid in the V direction.

        """
        return [self.point_at(i, j) for i, j in product(self.u_space(nu), self.v_space(nv))]

    def point_at(self, u, v):
        """Compute a point on the surface.

        Parameters
        ----------
        u : float
        v : float

        Returns
        -------
        :class:`~compas.geometry.Point`

        """
        raise NotImplementedError

    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`

        """
        raise NotImplementedError

    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`

        """
        raise NotImplementedError

    # ==============================================================================
    # Methods continued
    # ==============================================================================

    def closest_point(self, point, return_parameters=False):
        """Compute the closest point on the curve to a given point.

        Parameters
        ----------
        point : Point
            The point to project to the surface.
        return_parameters : bool, optional
            If True, return the surface UV parameters in addition to the closest point.

        Returns
        -------
        :class:`~compas.geometry.Point` | tuple[:class:`~compas.geometry.Point`, tuple[float, float]]
            If `return_parameters` is False, the nearest point on the surface.
            If `return_parameters` is True, the UV parameters in addition to the nearest point on the surface.

        """
        raise NotImplementedError

    def aabb(self, precision=0.0, optimal=False):
        """Compute the axis aligned bounding box of the surface.

        Parameters
        ----------
        precision : float, optional
        optimal : bool, optional

        Returns
        -------
        :class:`~compas.geometry.Box`

        """
        raise NotImplementedError

    def obb(self, precision=0.0):
        """Compute the oriented bounding box of the surface.

        Parameters
        ----------
        precision : float, optional

        Returns
        -------
        :class:`~compas.geometry.Box`

        """
        raise NotImplementedError

    def intersections_with_line(self, line):
        """Compute the intersections with a line.

        Parameters
        ----------
        line : :class:`~compas.geometry.Line`

        Returns
        -------
        list[:class:`~compas.geometry.Point`]

        """
        raise NotImplementedError

    def intersections_with_curve(self, curve):
        """Compute the intersections with a curve.

        Parameters
        ----------
        line : :class:`~compas.geometry.Curve`

        Returns
        -------
        list[:class:`~compas.geometry.Point`]

        """
        raise NotImplementedError

    def intersections_with_plane(self, plane):
        """Compute the intersections with a plane.

        Parameters
        ----------
        plane : :class:`~compas.geometry.Plane`

        Returns
        -------
        list[:class:`~compas.geometry.Curve`]

        """
        raise NotImplementedError

    # def patch(self, u, v, du=1, dv=1):
    #     """Construct a NURBS surface patch from the surface at the given UV parameters.

    #     Parameters
    #     ----------
    #     u : float
    #     v : float
    #     du : int, optional
    #     dv : int, optional

    #     Returns
    #     -------
    #     :class:`~compas.geometry.NurbsSurface`

    #     """
    #     raise NotImplementedError


# src/compas/geometry/surfaces/toroidal.py

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

from math import cos, sin, pi

from compas.geometry import Frame
from .surface import Surface

PI2 = 2 * pi


class ToroidalSurface(Surface):
    """A spherical surface is defined by a radius and a frame.

    Parameters
    ----------
    radius : float
        The radius of the sphere.
    frame : :class:`Frame`
        The frame of the sphere.

    Examples
    --------
    >>> from compas.geometry import Frame
    >>> from compas.geometry import ToroidalSurface
    >>> frame = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0])
    >>> sphere = ToroidalSurface(1.0, frame)

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "radius_axis": {"type": "number", "minimum": 0},
            "radius_pipe": {"type": "number", "minimum": 0},
            "frame": Frame.JSONSCHEMA,
        },
        "required": ["radius", "frame"],
    }

    # overwriting the __new__ method is necessary
    # to avoid triggering the plugin mechanism of the base surface class
    def __new__(cls, *args, **kwargs):
        surface = object.__new__(cls)
        surface.__init__(*args, **kwargs)
        return surface

    def __init__(self, radius_axis, radius_pipe, frame=None, **kwargs):
        super(ToroidalSurface, self).__init__(frame=frame, **kwargs)
        self._radius_axis = None
        self._radius_pipe = None
        self.radius_axis = radius_axis
        self.radius_pipe = radius_pipe

    def __repr__(self):
        return "ToroidalSurface(radius_axis={0!r}, radius_pipe={1!r}, frame={2!r})".format(
            self.radius_axis, self.radius_pipe, self.frame
        )

    def __eq__(self, other):
        try:
            other_frame = other.frame
            other_radius_axis = other.radius_axis
            other_radius_pipe = other.radius_pipe
        except Exception:
            return False
        return (
            self.radius_axis == other_radius_axis
            and self.radius_pipe == other_radius_pipe
            and self.frame == other_frame
        )

    @property
    def data(self):
        return {"radius_axis": self.radius_axis, "radius_pipe": self.radius_pipe, "frame": self.frame}

    @data.setter
    def data(self, data):
        self.radius_axis = data["radius_axis"]
        self.radius_pipe = data["radius_pipe"]
        self.frame = data["frame"]

    @property
    def center(self):
        return self.frame.point

    @center.setter
    def center(self, point):
        self.frame.point = point

    @property
    def radius_axis(self):
        if self._radius_axis is None:
            raise ValueError("The radius of the surface main axis has not been set yet.")
        return self._radius_axis

    @radius_axis.setter
    def radius_axis(self, radius):
        if radius < 0:
            raise ValueError("The radius of the main axis should be larger than or equal to zero.")
        self._radius_axis = float(radius)

    # =============================================================================
    # Constructors
    # =============================================================================

    @classmethod
    def from_plane_and_radius(cls, plane, radius_axis, radius_pipe):
        """Construct a sphere from a plane and a radius.

        Parameters
        ----------
        plane : :class:`compas.geometry.Plane`
            The plane of the sphere.
        radius : float
            The radius of the sphere.

        Returns
        -------
        :class:`compas.geometry.ToroidalSurface`
            A sphere.

        """
        return cls(radius_axis, radius_pipe, frame=Frame.from_plane(plane))

    # =============================================================================
    # Conversions
    # =============================================================================

    # =============================================================================
    # Methods
    # =============================================================================

    def point_at(self, u, v):
        """Construct a point on the sphere.

        Parameters
        ----------
        u : float
            The first parameter.
            The parameter value should be between zero and one,
            and will be mapped to the corresponding angle between zero and pi.
        v : float
            The second parameter.
            The parameter value should be between zero and one,
            and will be mapped to the corresponding angle between zero and 2 * pi.

        Returns
        -------
        :class:`compas.geometry.Point`
            A point on the sphere.

        """
        u = u * PI2
        v = v * PI2
        x = (self.radius_axis + self.radius_pipe * cos(v)) * cos(u)
        y = (self.radius_axis + self.radius_pipe * cos(v)) * sin(u)
        z = self.radius_pipe * sin(v)
        return self.frame.point + self.frame.xaxis * x + self.frame.yaxis * y + self.frame.zaxis * z

    def normal_at(self, u, v):
        """Construct a normal vector at a point on the sphere.

        Parameters
        ----------
        u : float
            The first parameter.
        v : float
            The second parameter.

        Returns
        -------
        :class:`compas.geometry.Vector`
            The normal vector.

        """
        raise NotImplementedError


# src/compas/geometry/surfaces/cylindrical.py
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from math import pi, cos, sin
from compas.geometry import Frame
from compas.geometry import Circle
from .surface import Surface

PI2 = 2 * pi


class CylindricalSurface(Surface):
    """A cylindrical surface is defined by a radius and a local coordinate system.

    Parameters
    ----------
    radius : float
        The radius of the cylinder.
    frame : :class:`Frame`
        The local coordinate system of the cylinder.

    Attributes
    ----------

    Examples
    --------

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "radius": {"type": "number", "minimum": 0},
            "frame": Frame.JSONSCHEMA,
        },
        "required": ["radius", "frame"],
    }

    # overwriting the __new__ method is necessary
    # to avoid triggering the plugin mechanism of the base surface class
    def __new__(cls, *args, **kwargs):
        surface = object.__new__(cls)
        surface.__init__(*args, **kwargs)
        return surface

    def __init__(self, radius, frame=None, **kwargs):
        super(CylindricalSurface, self).__init__(frame=frame, **kwargs)
        self._radius = None
        self.radius = radius

    def __repr__(self):
        return "CylindricalSurface(radius={0!r}, frame={1!r})".format(self.radius, self.frame)

    def __eq__(self, other):
        try:
            other_frame = other.frame
            other_radius = other.radius
        except Exception:
            return False
        return self.radius == other_radius and self.frame == other_frame

    @property
    def data(self):
        return {"radius": self.radius, "frame": self.frame}

    @data.setter
    def data(self, data):
        self.radius = data["radius"]
        self.frame = data["frame"]

    @property
    def center(self):
        return self.frame.point

    @center.setter
    def center(self, point):
        self.frame.point = point

    @property
    def radius(self):
        if self._radius is None:
            raise ValueError("The radius of the surface has not been set yet.")
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("The radius of a sphere should be larger than or equal to zero.")
        self._radius = float(radius)

    @property
    def area(self):
        raise NotImplementedError

    @property
    def volume(self):
        raise NotImplementedError

    # =============================================================================
    # Constructors
    # =============================================================================

    @classmethod
    def from_plane_and_radius(cls, plane, radius):
        """Construct a cylindrical surface from a plane and a radius.

        Parameters
        ----------
        plane : :class:`compas.geometry.Plane`
            The plane of the surface.
        radius : float
            The radius of the surface.

        Returns
        -------
        :class:`compas.geometry.CylindricalSurface`
            A cylindrical surface.

        """
        return cls(radius, frame=Frame.from_plane(plane))

    @classmethod
    def from_three_points(cls, a, b, c):
        """Construct a cylindrical from three points.

        Parameters
        ----------
        a : :class:`compas.geometry.Point`
            The first point.
        b : :class:`compas.geometry.Point`
            The second point.
        c : :class:`compas.geometry.Point`
            The third point.

        Returns
        -------
        :class:`compas.geometry.CylindricalSurface`
            A cylindrical surface.

        """
        circle = Circle.from_three_points(a, b, c)
        return cls(circle.radius, frame=circle.frame)

    # =============================================================================
    # Conversions
    # =============================================================================

    # =============================================================================
    # Transformations
    # =============================================================================

    # =============================================================================
    # Methods
    # =============================================================================

    def point_at(self, u, v):
        """Compute a point on the surface at the given parameters.

        Parameters
        ----------
        u : float
            The first parameter.
        v : float
            The second parameter.

        Returns
        -------
        :class:`compas.geometry.Point`
            The point at the given parameters.

        """
        u = u * PI2
        x = self.radius * cos(u)
        y = self.radius * sin(u)
        z = v
        return self.frame.point + self.frame.xaxis * x + self.frame.yaxis * y + self.frame.zaxis * z

    def normal_at(self, u):
        """Compute the normal at a point on the surface at the given parameters.

        Parameters
        ----------
        u : float
            The first parameter.

        Returns
        -------
        :class:`compas.geometry.Vector`
            The normal at the given parameters.

        """
        u = u * PI2
        x = self.radius * cos(u)
        y = self.radius * sin(u)
        z = 0
        return self.frame.xaxis * x + self.frame.yaxis * y + self.frame.zaxis * z

    def frame_at(self, u, v):
        """Compute the frame at a point on the surface at the given parameters.

        Parameters
        ----------
        u : float
            The first parameter.
        v : float
            The second parameter.

        Returns
        -------
        :class:`compas.geometry.Frame`
            The frame at the given parameters.

        """
        u = u * PI2
        point = self.point_at(u, v)
        zaxis = self.normal_at(u)
        yaxis = self.frame.zaxis
        xaxis = yaxis.cross(zaxis)
        return Frame(point, xaxis, yaxis)
