
                    matrices.append(
                        np.linalg.inv(backward['matrix']))
            # filter out any identity matrices
            matrices = [m for m in matrices if
                        np.abs((m - _identity)).max() > 1e-8]
            if len(matrices) == 0:
                matrix = _identity
            elif len(matrices) == 1:
                matrix = matrices[0]
            else:
                # multiply matrices into single transform
                matrix = util.multi_dot(matrices)

        # if instructed to repair rigid transforms do it here
        if self.repair_rigid is not None:
            matrix = fix_rigid(matrix, max_deviance=self.repair_rigid)

        # matrix being edited in-place leads to subtle bugs
        matrix.flags['WRITEABLE'] = False

        # store the result
        self._cache[key] = (matrix, geometry)

        return matrix, geometry

    def __hash__(self):
        return self.transforms.__hash__()

    def copy(self):
        """
        Return a copy of the current TransformForest.

        Returns
        ------------
        copied : TransformForest
          Copy of current object.
        """
        # create a copy without transferring cache
        copied = SceneGraph()
        copied.base_frame = deepcopy(self.base_frame)
        copied.transforms = deepcopy(self.transforms)
        return copied

    def to_flattened(self):
        """
        Export the current transform graph with all
        transforms baked into world->instance.

        Returns
        ---------
        flat : dict
          Keyed {node : {transform, geometry}
        """
        flat = {}
        base_frame = self.base_frame
        for node in self.nodes:
            if node == base_frame:
                continue
            # get the matrix and geometry name
            matrix, geometry = self.get(
                frame_to=node, frame_from=base_frame)
            # store matrix as list rather than numpy array
            flat[node] = {'transform': matrix.tolist(),
                          'geometry': geometry}

        return flat

    def to_gltf(self, scene, mesh_index=None):
        """
        Export a transforms as the 'nodes' section of the
        GLTF header dict.

        Parameters
        ------------
        scene : trimesh.Scene
          Scene with geometry.
        mesh_index : dict or None
          Mapping { key in scene.geometry : int }

        Returns
        --------
        gltf : dict
          With 'nodes' referencing a list of dicts
        """

        if mesh_index is None:
            # geometry is an OrderedDict
            # map mesh name to index: {geometry key : index}
            mesh_index = {name: i for i, name
                          in enumerate(scene.geometry.keys())}

        # get graph information into local scope before loop
        graph = self.transforms
        # get the stored node data
        node_data = graph.node_data
        edge_data = graph.edge_data
        base_frame = self.base_frame

        # list of dict, in gltf format
        # start with base frame as first node index
        result = [{'name': base_frame}]
        # {node name : node index in gltf}
        lookup = {base_frame: 0}

        # collect the nodes in order
        for node in node_data.keys():
            if node == base_frame:
                continue
            # assign the index to the node-name lookup
            lookup[node] = len(result)
            # populate a result at the correct index
            result.append({'name': node})

        # get generated properties outside of loop
        # does the scene have a defined camera to export
        has_camera = scene.has_camera
        children = graph.children

        extensions_used = set()

        # then iterate through to collect data
        for info in result:
            # name of the scene node
            node = info['name']

            # get the original node names for children
            childs = children.get(node, [])
            if len(childs) > 0:
                info['children'] = [lookup[k] for k in childs]

            # if we have a mesh store by index
            if 'geometry' in node_data[node]:
                mesh_key = node_data[node]['geometry']
                if mesh_key in mesh_index:
                    info['mesh'] = mesh_index[mesh_key]
            # check to see if we have camera node
            if has_camera and node == scene.camera.name:
                info['camera'] = 0

            if node != base_frame:
                parent = graph.parents[node]
                node_edge = edge_data[(parent, node)]

                # get the matrix from this edge
                matrix = node_edge['matrix']
                # only include if it's not an identify matrix
                if not util.allclose(matrix, _identity):
                    info['matrix'] = matrix.T.reshape(-1).tolist()

                # if an extra was stored on this edge
                extras = node_edge.get('metadata')
                if extras:
                    extras = extras.copy()

                    # if extensionss were stored on this edge
                    extensions = extras.pop('gltf_extensions', None)
                    if isinstance(extensions, dict):
                        info['extensions'] = extensions
                        extensions_used = extensions_used.union(
                            set(extensions.keys()))

                    # convert any numpy arrays to lists
                    extras.update(
                        {k: v.tolist() for k, v in extras.items()
                         if hasattr(v, 'tolist')})
                    info['extras'] = extras

        gltf = {'nodes': result}
        if len(extensions_used) > 0:
            gltf['extensionsUsed'] = list(extensions_used)
        return gltf

    def to_edgelist(self):
        """
        Export the current transforms as a list of
        edge tuples, with each tuple having the format:
        (node_a, node_b, {metadata})

        Returns
        ---------
        edgelist : (n,) list
          Of edge tuples
        """
        # save local reference to node_data
        nodes = self.transforms.node_data
        # save cleaned edges
        export = []
        # loop through (node, node, edge attributes)
        for edge, attr in self.transforms.edge_data.items():
            # node indexes from edge
            a, b = edge
            # geometry is a node property but save it to the
            # edge so we don't need two dictionaries
            b_attr = nodes[b]
            # make sure we're not stomping on original
            attr_new = attr.copy()
            # apply node geometry to edge attributes
            if 'geometry' in b_attr:
                attr_new['geometry'] = b_attr['geometry']
            # convert any numpy arrays to regular lists
            attr_new.update(
                {k: v.tolist() for k, v in attr_new.items()
                 if hasattr(v, 'tolist')})
            export.append([a, b, attr_new])
        return export

    def from_edgelist(self, edges, strict=True):
        """
        Load transform data from an edge list into the current
        scene graph.

        Parameters
        -------------
        edgelist : (n,) tuples
          Keyed (node_a, node_b, {key: value})
        strict : bool
          If True raise a ValueError when a
          malformed edge is passed in a tuple.
        """

        # loop through each edge
        for edge in edges:
            # edge contains attributes
            if len(edge) == 3:
                self.update(edge[1], edge[0], **edge[2])
            # edge just contains nodes
            elif len(edge) == 2:
                self.update(edge[1], edge[0])
            # edge is broken
            elif strict:
                raise ValueError(
                    'edge incorrect shape: %s', str(edge))

    def to_networkx(self):
        """
        Return a `networkx` copy of this graph.

        Returns
        ----------
        graph : networkx.DiGraph
          Directed graph.
        """
        import networkx
        return networkx.from_edgelist(
            self.to_edgelist(),
            create_using=networkx.DiGraph)

    def show(self, **kwargs):
        """
        Plot the scene graph using `networkx.draw_networkx`
        which uses matplotlib to display the graph.

        Parameters
        -----------
        kwargs : dict
          Passed to `networkx.draw_networkx`
        """
        import networkx
        import matplotlib.pyplot as plt
        # default kwargs will only be set if not
        # passed explicitly to the show command
        defaults = {'with_labels': True}
        kwargs.update(**{k: v for k, v in defaults.items()
                         if k not in kwargs})
        networkx.draw_networkx(
            G=self.to_networkx(),
            **kwargs)

        plt.show()

    def load(self, edgelist):
        """
        Load transform data from an edge list into the current
        scene graph.

        Parameters
        -------------
        edgelist : (n,) tuples
          Structured (node_a, node_b, {key: value})
        """
        self.from_edgelist(edgelist, strict=True)

    @caching.cache_decorator
    def nodes(self):
        """
        A list of every node in the graph.

        Returns
        -------------
        nodes : (n,) array
          All node names.
        """
        return self.transforms.nodes

    @caching.cache_decorator
    def nodes_geometry(self):
        """
        The nodes in the scene graph with geometry attached.

        Returns
        ------------
        nodes_geometry : (m,) array
          Node names which have geometry associated
        """
        return [n for n, attr in
                self.transforms.node_data.items()
                if 'geometry' in attr]

    @caching.cache_decorator
    def geometry_nodes(self):
        """
        Which nodes have this geometry? Inverse
        of `nodes_geometry`.

        Returns
        ------------
        geometry_nodes : dict
          Keyed {geometry_name : node name}
        """
        res = collections.defaultdict(list)
        for node, attr in self.transforms.node_data.items():
            if 'geometry' in attr:
                res[attr['geometry']].append(node)
        return res

    def remove_geometries(self, geometries):
        """
        Remove the reference for specified geometries
        from nodes without deleting the node.

        Parameters
        ------------
        geometries : list or str
          Name of scene.geometry to dereference.
        """
        # make sure we have a set of geometries to remove
        if util.is_string(geometries):
            geometries = [geometries]
        geometries = set(geometries)

        # remove the geometry reference from the node without deleting nodes
        # this lets us keep our cached paths, and will not screw up children
        for attrib in self.transforms.node_data.values():
            if 'geometry' in attrib and attrib['geometry'] in geometries:
                attrib.pop('geometry')

        # it would be safer to just run _cache.clear
        # but the only property using the geometry should be
        # nodes_geometry: if this becomes not true change this to clear!
        self._cache.cache.pop('nodes_geometry', None)

    def __contains__(self, key):
        return key in self.transforms.node_data

    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, value):
        value = np.asanyarray(value)
        if value.shape != (4, 4):
            raise ValueError('Matrix must be specified!')
        return self.update(key, matrix=value)

    def clear(self):
        self.transforms = EnforcedForest()
        self._cache.clear()


class EnforcedForest(object):
    """
    A simple forest graph data structure: every node
    is allowed to have exactly one parent. This makes
    traversal and implementation much simpler than a
    full graph data type; by storing only one parent
    reference, it enforces the structure for "free."
    """

    def __init__(self):
        # since every node can have only one parent
        # this data structure transparently enforces
        # the forest data structure without checks
        # a dict {child : parent}
        self.parents = {}

        # store data for a particular edge keyed by tuple
        # {(u, v) : data }
        self.edge_data = collections.defaultdict(dict)
        # {u: data}
        self.node_data = collections.defaultdict(dict)

        # if multiple calls are made for the same path
        # but the connectivity hasn't changed return cached
        self._cache = {}

    def add_edge(self, u, v, **kwargs):
        """
        Add an edge to the forest cleanly.

        Parameters
        -----------
        u : any
          Hashable node key.
        v : any
          Hashable node key.
        kwargs : dict
           Stored as (u, v) edge data.

        Returns
        --------
        changed : bool
          Return if this operation changed anything.
       """
        self._hash = None

        # topology has changed so clear cache
        if (u, v) not in self.edge_data:
            self._cache = {}
        else:
            # check to see if matrix and geometry are identical
            edge = self.edge_data[(u, v)]
            if (util.allclose(kwargs.get('matrix', _identity),
                              edge.get('matrix', _identity),
                              1e-8)
                and (edge.get('geometry') ==
                     kwargs.get('geometry'))):
                return False

        # store a parent reference for traversal
        self.parents[v] = u
        # store kwargs for edge data keyed with tuple
        self.edge_data[(u, v)] = kwargs
        # set empty node data
        self.node_data[u].update({})
        if 'geometry' in kwargs:
            self.node_data[v].update(
                {'geometry': kwargs['geometry']})
        else:
            self.node_data[v].update({})

        return True

    def remove_node(self, u):
        """
        Remove a node from the forest.

        Parameters
        -----------
        u : any
          Hashable node key.

        Returns
        --------
        changed : bool
          Return if this operation changed anything.
        """
        # check if node is part of forest
        if u not in self.node_data:
            return False

        # topology will change so clear cache
        self._cache = {}
        self._hash = None

        # delete all children's references and parent reference
        children = [
            child for (
                child,
                parent) in self.parents.items() if parent == u]
        for c in children:
            del self.parents[c]
        if u in self.parents:
            del self.parents[u]

        # delete edge data
        edges = [(a, b) for (a, b) in self.edge_data if a == u or b == u]
        for e in edges:
            del self.edge_data[e]

        # delete node data
        del self.node_data[u]

        return True

    def shortest_path(self, u, v):
        """
        Find the shortest path between `u` and `v`, returning
        a path where the first element is always `u` and the
        last element is always `v`, disregarding edge direction.

        Parameters
        -----------
        u : any
          Hashable node key.
        v : any
          Hashable node key.

        Returns
        -----------
        path : (n,)
          Path between `u` and `v`
        """
        # see if we've already computed this path
        if u == v:
            #  the path between itself is an edge case
            return []
        elif (u, v) in self._cache:
            # return the same path for either direction
            return self._cache[(u, v)]
        elif (v, u) in self._cache:
            return self._cache[(v, u)][::-1]

        # local reference to parent dict for performance
        parents = self.parents
        # store both forward and backwards traversal
        forward = [u]
        backward = [v]

        # cap iteration to number of total nodes
        for _ in range(len(parents) + 1):
            # store the parent both forwards and backwards
            f = parents.get(forward[-1])
            b = parents.get(backward[-1])
            forward.append(f)
            backward.append(b)

            if f == v:
                self._cache[(u, v)] = forward
                return forward
            elif b == u:
                # return reversed path
                backward = backward[::-1]
                self._cache[(u, v)] = backward
                return backward
            elif (b in forward) or (f is None and b is None):
                # we have a either a common node between both
                # traversal directions or we have consumed the whole
                # tree in both directions so try to find the common node
                common = set(backward).intersection(
                    forward).difference({None})
                if len(common) == 0:
                    raise ValueError('No path from {}->{}!'.format(u, v))
                elif len(common) > 1:
                    # get the first occurring common element in "forward"
                    link = next(f for f in forward if f in common)
                    assert link in common
                else:
                    # take the only common element
                    link = next(iter(common))

                # combine the forward and backwards traversals
                a = forward[:forward.index(link) + 1]
                b = backward[:backward.index(link)]
                path = a + b[::-1]

                # verify we didn't screw up the order
                assert path[0] == u
                assert path[-1] == v

                self._cache[(u, v)] = path

                return path

        raise ValueError('Iteration limit exceeded!')

    @property
    def nodes(self):
        """
        Get a set of every node.

        Returns
        -----------
        nodes : set
          Every node currently stored.
        """
        return self.node_data.keys()

    @property
    def children(self):
        """
        Get the children of each node.

        Returns
        ----------
        children : dict
          Keyed {node : [child, child, ...]}
        """
        if 'children' in self._cache:
            return self._cache['children']
        child = collections.defaultdict(list)
        # append children to parent references
        # skip self-references to avoid a node loop
        [child[v].append(u) for u, v in
         self.parents.items() if u != v]

        # cache and return as a vanilla dict
        self._cache['children'] = dict(child)
        return self._cache['children']

    def successors(self, node):
        """
        Get all nodes that are successors to specified node,
        including the specified node.

        Parameters
        -------------
        node : any
          Hashable key for a node.

        Returns
        ------------
        successors : set
          Nodes that succeed specified node.
        """
        # get mapping of {parent : child}
        children = self.children
        # if node doesn't exist return early
        if node not in children:
            return set([node])

        # children we need to collect
        queue = [node]
        # start collecting values with children of source
        collected = set(queue)

        # cap maximum iterations
        for _ in range(len(self.node_data) + 1):
            if len(queue) == 0:
                # no more nodes to visit so we're done
                return collected
            # add the children of this node to be processed
            childs = children.get(queue.pop())
            if childs is not None:
                queue.extend(childs)
                collected.update(childs)
        return collected

    def __hash__(self):
        """
        Actually hash all of the data, but use a "dirty" mechanism
        in functions that modify the data, which MUST
        # all invalidate the hash by setting `self._hash = None`

        This was optimized a bit, and is evaluating on an
        older laptop on a scene with 77 nodes and 76 edges
        10,000 times in 0.7s which seems fast enough.
        """
        # see if there is an available hash value
        # if you are seeing cache bugs this is the thing
        # to try eliminating because it is very likely that
        # someone somewhere is modifying the data without
        # setting `self._hash = None`
        hashed = getattr(self, '_hash', None)
        if hashed is not None:
            return hashed

        hashed = hash_fast(
            (''.join(str(hash(k)) + v.get('geometry', '')
                     for k, v in self.edge_data.items()) +
             ''.join(str(k) + v.get('geometry', '')
                     for k, v in self.node_data.items())).encode('utf-8') +
            b''.join(v['matrix'].tobytes()
                     for v in self.edge_data.values()
                     if 'matrix' in v))
        self._hash = hashed
        return hashed


def kwargs_to_matrix(
        matrix=None,
        quaternion=None,
        translation=None,
        axis=None,
        angle=None,
        **kwargs):
    """
    Take multiple keyword arguments and parse them
    into a homogeneous transformation matrix.

    Returns
    ---------
    matrix : (4, 4) float
      Homogeneous transformation matrix.
    """
    if matrix is not None:
        # a matrix takes immediate precedence over other options
        return np.array(matrix, dtype=np.float64)
    elif quaternion is not None:
        matrix = quaternion_matrix(quaternion)
    elif axis is not None and angle is not None:
        matrix = rotation_matrix(angle, axis)
    else:
        matrix = np.eye(4)

    if translation is not None:
        # translation can be used in conjunction with any
        # of the methods specifying transforms
        matrix[:3, 3] += translation

    return matrix


# trimesh/viewer/__init__.py

"""
viewer
-------------

View meshes and scenes via pyglet or inline HTML.
"""


from .notebook import (in_notebook,
                       scene_to_notebook,
                       scene_to_html)

from .. import exceptions

try:
    # try importing windowed which will fail
    # if we can't create an openGL context
    from .windowed import (SceneViewer,
                           render_scene)
except BaseException as E:
    # if windowed failed to import only raise
    # the exception if someone tries to use them
    SceneViewer = exceptions.ExceptionWrapper(E)
    render_scene = exceptions.ExceptionWrapper(E)


try:
    from .widget import SceneWidget
except BaseException as E:
    SceneWidget = exceptions.ExceptionWrapper(E)


# this is only standard library imports

# explicitly list imports in __all__
# as otherwise flake8 gets mad
__all__ = ['SceneWidget',
           'SceneViewer',
           'render_scene',
           'in_notebook',
           'scene_to_notebook',
           'scene_to_html']


# trimesh/viewer/notebook.py

"""
notebook.py
-------------

Render trimesh.Scene objects in HTML
and jupyter notebooks using three.js
"""
import os
import base64

# for our template
from .. import util
from .. import resources


def scene_to_html(scene):
    """
    Return HTML that will render the scene using
    GLTF/GLB encoded to base64 loaded by three.js

    Parameters
    --------------
    scene : trimesh.Scene
      Source geometry

    Returns
    --------------
    html : str
      HTML containing embedded geometry
    """
    # fetch HTML template from ZIP archive
    # it is bundling all of three.js so compression is nice
    base = util.decompress(
        resources.get('templates/viewer.zip', decode=False),
        file_type='zip')['viewer.html.template'].read().decode('utf-8')
    scene.camera
    # get export as bytes
    data = scene.export(file_type='glb')
    # encode as base64 string
    encoded = base64.b64encode(data).decode('utf-8')
    # replace keyword with our scene data
    result = base.replace('$B64GLTF', encoded)

    return result


def scene_to_notebook(scene, height=500, **kwargs):
    """
    Convert a scene to HTML containing embedded geometry
    and a three.js viewer that will display nicely in
    an IPython/Jupyter notebook.

    Parameters
    -------------
    scene : trimesh.Scene
      Source geometry

    Returns
    -------------
    html : IPython.display.HTML
      Object containing rendered scene
    """
    # keep as soft dependency
    from IPython import display

    # convert scene to a full HTML page
    as_html = scene_to_html(scene=scene)

    # escape the quotes in the HTML
    srcdoc = as_html.replace('"', '&quot;')
    # embed this puppy as the srcdoc attr of an IFframe
    # I tried this a dozen ways and this is the only one that works
    # display.IFrame/display.Javascript really, really don't work
    # div is to avoid IPython's pointless hardcoded warning
    embedded = display.HTML(' '.join([
        '<div><iframe srcdoc="{srcdoc}"',
        'width="100%" height="{height}px"',
        'style="border:none;"></iframe></div>']).format(
            srcdoc=srcdoc,
            height=height))
    return embedded


def in_notebook():
    """
    Check to see if we are in an IPython or Jypyter notebook.

    Returns
    -----------
    in_notebook : bool
      Returns True if we are in a notebook
    """
    try:
        # function returns IPython context, but only in IPython
        ipy = get_ipython()  # NOQA
        # we only want to render rich output in notebooks
        # in terminals we definitely do not want to output HTML
        name = str(ipy.__class__).lower()
        terminal = 'terminal' in name

        # spyder uses ZMQshell, and can appear to be a notebook
        spyder = '_' in os.environ and 'spyder' in os.environ['_']

        # assume we are in a notebook if we are not in
        # a terminal and we haven't been run by spyder
        notebook = (not terminal) and (not spyder)

        return notebook

    except BaseException:
        return False


# trimesh/viewer/trackball.py

# Copied from
# https://github.com/mmatl/pyrender/blob/master/pyrender/trackball.py

# MIT License
#
# Copyright (c) 2019 Matthew Matl
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Trackball class for 3D manipulation of viewpoints.
"""
import numpy as np

from .. import transformations


class Trackball(object):
    """A trackball class for creating camera transforms from mouse movements.
    """
    STATE_ROTATE = 0
    STATE_PAN = 1
    STATE_ROLL = 2
    STATE_ZOOM = 3

    def __init__(self, pose, size, scale, target=None):
        """Initialize a trackball with an initial camera-to-world pose
        and the given parameters.

        Parameters
        ----------
        pose : [4,4]
            An initial camera-to-world pose for the trackball.

        size : (float, float)
            The width and height of the camera image in pixels.

        scale : float
            The diagonal of the scene's bounding box --
            used for ensuring translation motions are sufficiently
            fast for differently-sized scenes.

        target : (3,) float
            The center of the scene in world coordinates.
            The trackball will revolve around this point.
        """
        self._size = np.array(size)
        self._scale = float(scale)

        self._pose = pose
        self._n_pose = pose

        if target is None:
            self._target = np.array([0.0, 0.0, 0.0])
            self._n_target = np.array([0.0, 0.0, 0.0])
        else:
            self._target = target
            self._n_target = target

        self._state = Trackball.STATE_ROTATE

    @property
    def pose(self):
        """autolab_core.RigidTransform : The current camera-to-world pose.
        """
        return self._n_pose

    def set_state(self, state):
        """Set the state of the trackball in order to change the effect of
        dragging motions.

        Parameters
        ----------
        state : int
            One of Trackball.STATE_ROTATE, Trackball.STATE_PAN,
            Trackball.STATE_ROLL, and Trackball.STATE_ZOOM.
        """
        self._state = state

    def resize(self, size):
        """Resize the window.

        Parameters
        ----------
        size : (float, float)
            The new width and height of the camera image in pixels.
        """
        self._size = np.array(size)

    def down(self, point):
        """Record an initial mouse press at a given point.

        Parameters
        ----------
        point : (2,) int
            The x and y pixel coordinates of the mouse press.
        """
        self._pdown = np.array(point, dtype=np.float32)
        self._pose = self._n_pose
        self._target = self._n_target

    def drag(self, point):
        """Update the tracball during a drag.

        Parameters
        ----------
        point : (2,) int
            The current x and y pixel coordinates of the mouse during a drag.
            This will compute a movement for the trackball with the relative
            motion between this point and the one marked by down().
        """
        point = np.array(point, dtype=np.float32)
        # get the "down" point defaulting to current point making
        # this a no-op if the "down" event didn't trigger for some reason
        dx, dy = point - getattr(self, '_pdown', point)
        mindim = 0.3 * np.min(self._size)

        target = self._target
        x_axis = self._pose[:3, 0].flatten()
        y_axis = self._pose[:3, 1].flatten()
        z_axis = self._pose[:3, 2].flatten()
        eye = self._pose[:3, 3].flatten()

        # Interpret drag as a rotation
        if self._state == Trackball.STATE_ROTATE:
            x_angle = -dx / mindim
            x_rot_mat = transformations.rotation_matrix(
                x_angle, y_axis, target
            )

            y_angle = dy / mindim
            y_rot_mat = transformations.rotation_matrix(
                y_angle, x_axis, target
            )

            self._n_pose = y_rot_mat.dot(x_rot_mat.dot(self._pose))

        # Interpret drag as a roll about the camera axis
        elif self._state == Trackball.STATE_ROLL:
            center = self._size / 2.0
            v_init = self._pdown - center
            v_curr = point - center
            v_init = v_init / np.linalg.norm(v_init)
            v_curr = v_curr / np.linalg.norm(v_curr)

            theta = (-np.arctan2(v_curr[1], v_curr[0]) +
                     np.arctan2(v_init[1], v_init[0]))

            rot_mat = transformations.rotation_matrix(theta, z_axis, target)

            self._n_pose = rot_mat.dot(self._pose)

        # Interpret drag as a camera pan in view plane
        elif self._state == Trackball.STATE_PAN:
            dx = -dx / (5.0 * mindim) * self._scale
            dy = -dy / (5.0 * mindim) * self._scale

            translation = dx * x_axis + dy * y_axis
            self._n_target = self._target + translation
            t_tf = np.eye(4)
            t_tf[:3, 3] = translation
            self._n_pose = t_tf.dot(self._pose)

        # Interpret drag as a zoom motion
        elif self._state == Trackball.STATE_ZOOM:
            radius = np.linalg.norm(eye - target)
            ratio = 0.0
            if dy > 0:
                ratio = np.exp(abs(dy) / (0.5 * self._size[1])) - 1.0
            elif dy < 0:
                ratio = 1.0 - np.exp(dy / (0.5 * (self._size[1])))
            translation = -np.sign(dy) * ratio * radius * z_axis
            t_tf = np.eye(4)
            t_tf[:3, 3] = translation
            self._n_pose = t_tf.dot(self._pose)

    def scroll(self, clicks):
        """Zoom using a mouse scroll wheel motion.

        Parameters
        ----------
        clicks : int
            The number of clicks. Positive numbers indicate forward wheel
            movement.
        """
        target = self._target
        ratio = 0.90

        mult = 1.0
        if clicks > 0:
            mult = ratio**clicks
        elif clicks < 0:
            mult = (1.0 / ratio)**abs(clicks)

        z_axis = self._n_pose[:3, 2].flatten()
        eye = self._n_pose[:3, 3].flatten()
        radius = np.linalg.norm(eye - target)
        translation = (mult * radius - radius) * z_axis
        t_tf = np.eye(4)
        t_tf[:3, 3] = translation
        self._n_pose = t_tf.dot(self._n_pose)

        z_axis = self._pose[:3, 2].flatten()
        eye = self._pose[:3, 3].flatten()
        radius = np.linalg.norm(eye - target)
        translation = (mult * radius - radius) * z_axis
        t_tf = np.eye(4)
        t_tf[:3, 3] = translation
        self._pose = t_tf.dot(self._pose)

    def rotate(self, azimuth, axis=None):
        """Rotate the trackball about the "Up" axis by azimuth radians.

        Parameters
        ----------
        azimuth : float
            The number of radians to rotate.
        """
        target = self._target

        y_axis = self._n_pose[:3, 1].flatten()
        if axis is not None:
            y_axis = axis
        x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
        self._n_pose = x_rot_mat.dot(self._n_pose)

        y_axis = self._pose[:3, 1].flatten()
        if axis is not None:
            y_axis = axis
        x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
        self._pose = x_rot_mat.dot(self._pose)


# trimesh/viewer/widget.py

"""
widget.py
-------------

A widget which can visualize trimesh.Scene objects in a glooey window.

Check out an example in `examples/widget.py`
"""
import glooey
import numpy as np
import pyglet
from pyglet import gl

from .. import rendering
from .trackball import Trackball
from .windowed import geometry_hash
from .windowed import SceneViewer


class SceneGroup(pyglet.graphics.Group):

    def __init__(
        self,
        rect,
        scene,
        background=None,
        pixel_per_point=(1, 1),
        parent=None,
    ):
        super().__init__(parent)
        self.rect = rect
        self.scene = scene

        if background is None:
            background = [.99, .99, .99, 1.0]
        self._background = background

        self._pixel_per_point = pixel_per_point

    def _set_view(self):
        left = int(self._pixel_per_point[0] * self.rect.left)
        bottom = int(self._pixel_per_point[1] * self.rect.bottom)
        width = int(self._pixel_per_point[0] * self.rect.width)
        height = int(self._pixel_per_point[1] * self.rect.height)

        gl.glPushAttrib(gl.GL_ENABLE_BIT)
        gl.glEnable(gl.GL_SCISSOR_TEST)
        gl.glScissor(left, bottom, width, height)

        self._mode = (gl.GLint)()
        gl.glGetIntegerv(gl.GL_MATRIX_MODE, self._mode)
        self._viewport = (gl.GLint * 4)()
        gl.glGetIntegerv(gl.GL_VIEWPORT, self._viewport)

        gl.glViewport(left, bottom, width, height)
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glPushMatrix()
        gl.glLoadIdentity()
        near = 0.01
        far = 1000.
        gl.gluPerspective(self.scene.camera.fov[1], width / height, near, far)
        gl.glMatrixMode(gl.GL_MODELVIEW)

    def _unset_view(self):
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glPopMatrix()
        gl.glMatrixMode(self._mode.value)
        gl.glViewport(
            self._viewport[0],
            self._viewport[1],
            self._viewport[2],
            self._viewport[3],
        )

        gl.glPopAttrib()

    def set_state(self):
        self._set_view()

        SceneViewer._gl_set_background(self._background)
        SceneViewer._gl_enable_depth(self.scene.camera)
        SceneViewer._gl_enable_color_material()
        SceneViewer._gl_enable_blending()
        SceneViewer._gl_enable_smooth_lines()
        SceneViewer._gl_enable_lighting(self.scene)

        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)

        gl.glPushMatrix()
        gl.glLoadIdentity()
        gl.glMultMatrixf(
            rendering.matrix_to_gl(np.linalg.inv(self.scene.camera_transform)))

    def unset_state(self):
        gl.glPopMatrix()

        SceneViewer._gl_unset_background()

        self._unset_view()


class MeshGroup(pyglet.graphics.Group):

    def __init__(self, transform=None, texture=None, parent=None):
        super().__init__(parent)
        if transform is None:
            transform = np.eye(4)
        self.transform = transform
        self.texture = texture

    def set_state(self):
        gl.glPushMatrix()
        gl.glMultMatrixf(rendering.matrix_to_gl(self.transform))

        if self.texture:
            gl.glEnable(self.texture.target)
            gl.glBindTexture(self.texture.target, self.texture.id)

    def unset_state(self):
        if self.texture:
            gl.glDisable(self.texture.target)

        gl.glPopMatrix()


class SceneWidget(glooey.Widget):

    def __init__(self, scene, **kwargs):
        super().__init__()
        self.scene = scene
        self._scene_group = None

        # key is node_name
        self.mesh_group = {}

        # key is geometry_name
        self.vertex_list = {}
        self.vertex_list_hash = {}
        self.textures = {}

        self._initial_camera_transform = self.scene.camera_transform.copy()
        self.reset_view()

        self._background = kwargs.pop('background', None)
        self._smooth = kwargs.pop('smooth', True)
        if kwargs:
            raise TypeError('unexpected kwargs: {}'.format(kwargs))

    @property
    def scene_group(self):
        if self._scene_group is None:
            pixel_per_point = (np.array(self.window.get_viewport_size()) /
                               np.array(self.window.get_size()))
            self._scene_group = SceneGroup(
                rect=self.rect,
                scene=self.scene,
                background=self._background,
                pixel_per_point=pixel_per_point,
                parent=self.group,
            )
        return self._scene_group

    def clear(self):
        self._scene_group = None
        self.mesh_group = {}
        while self.vertex_list:
            _, vertex = self.vertex_list.popitem()
            vertex.delete()
        self.vertex_list_hash = {}
        self.textures = {}

    def reset_view(self):
        self.view = {
            'ball': Trackball(
                pose=self._initial_camera_transform,
                size=self.scene.camera.resolution,
                scale=self.scene.scale,
                target=self.scene.centroid)}
        self.scene.camera_transform = self.view['ball'].pose

    def do_claim(self):
        return 0, 0

    def do_regroup(self):
        if not self.vertex_list:
            return

        node_names = self.scene.graph.nodes_geometry
        for node_name in node_names:
            transform, geometry_name = self.scene.graph[node_name]
            if geometry_name not in self.vertex_list:
                continue
            vertex_list = self.vertex_list[geometry_name]

            if node_name in self.mesh_group:
                mesh_group = self.mesh_group[node_name]
            else:
                mesh_group = MeshGroup(
                    transform=transform,
                    texture=self.textures.get(geometry_name),
                    parent=self.scene_group)
                self.mesh_group[node_name] = mesh_group
            self.batch.migrate(
                vertex_list,
                gl.GL_TRIANGLES,
                mesh_group,
                self.batch)

    def do_draw(self):
        resolution = (self.rect.width, self.rect.height)
        if not (resolution == self.scene.camera.resolution).all():
            self.scene.camera.resolution = resolution

        node_names = self.scene.graph.nodes_geometry
        for node_name in node_names:
            transform, geometry_name = self.scene.graph[node_name]
            geometry = self.scene.geometry[geometry_name]
            self._update_node(node_name, geometry_name, geometry, transform)

    def do_undraw(self):
        if not self.vertex_list:
            return
        for vertex_list in self.vertex_list.values():
            vertex_list.delete()
        self._scene_group = None
        self.mesh_group = {}
        self.vertex_list = {}
        self.vertex_list_hash = {}
        self.textures = {}

    def on_mouse_press(self, x, y, buttons, modifiers):
        SceneViewer.on_mouse_press(self, x, y, buttons, modifiers)
        self._draw()

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        # detect a drag across widgets
        x_prev = x - dx
        y_prev = y - dy
        left, bottom = self.rect.left, self.rect.bottom
        width, height = self.rect.width, self.rect.height
        if not (left < x_prev <= left + width) or \
                not (bottom < y_prev <= bottom + height):
            self.view['ball'].down(np.array([x, y]))

        SceneViewer.on_mouse_drag(self, x, y, dx, dy, buttons, modifiers)
        self._draw()

    def on_mouse_scroll(self, x, y, dx, dy):
        SceneViewer.on_mouse_scroll(self, x, y, dx, dy)
        self._draw()

    def _update_node(self, node_name, geometry_name, geometry, transform):
        geometry_hash_new = geometry_hash(geometry)
        if self.vertex_list_hash.get(geometry_name) != geometry_hash_new:
            # if geometry has texture defined convert it to opengl form
            if hasattr(geometry, 'visual') and hasattr(
                    geometry.visual, 'material'):
                tex = rendering.material_to_texture(geometry.visual.material)
                if tex is not None:
                    self.textures[geometry_name] = tex

        if node_name in self.mesh_group:
            mesh_group = self.mesh_group[node_name]
            mesh_group.transform = transform
            mesh_group.texture = self.textures.get(geometry_name)
        else:
            mesh_group = MeshGroup(
                transform=transform,
                texture=self.textures.get(geometry_name),
                parent=self.scene_group)
            self.mesh_group[node_name] = mesh_group

        if self.vertex_list_hash.get(geometry_name) != geometry_hash_new:
            if geometry_name in self.vertex_list:
                self.vertex_list[geometry_name].delete()

            # convert geometry to constructor args
            args = rendering.convert_to_vertexlist(
                geometry,
                group=mesh_group,
                smooth=self._smooth)
            # create the indexed vertex list
            self.vertex_list[geometry_name] = self.batch.add_indexed(*args)
            # save the MD5 of the geometry
            self.vertex_list_hash[geometry_name] = geometry_hash_new


# trimesh/viewer/windowed.py

"""
windowed.py
---------------

Provides a pyglet- based windowed viewer to preview
Trimesh, Scene, PointCloud, and Path objects.

Works on all major platforms: Windows, Linux, and OSX.
"""
import collections
import numpy as np

import pyglet

# pyglet 2.0 is close to a re-write moving from fixed-function
# to shaders and we will likely support it by forking an entirely
# new viewer `trimesh.viewer.shaders` and then basically keeping
# `windowed` around for backwards-compatibility with no changes
if int(pyglet.version.split('.')[0]) >= 2:
    raise ImportError(
        '`trimesh.viewer.windowed` requires `pip install "pyglet<2"`')

from .trackball import Trackball

from .. import util
from .. import rendering

from ..visual import to_rgba
from ..transformations import translation_matrix

pyglet.options['shadow_window'] = False

import pyglet.gl as gl  # NOQA

# smooth only when fewer faces than this
_SMOOTH_MAX_FACES = 100000


class SceneViewer(pyglet.window.Window):

    def __init__(self,
                 scene,
                 smooth=True,
                 flags=None,
                 visible=True,
                 resolution=None,
                 start_loop=True,
                 callback=None,
                 callback_period=None,
                 caption=None,
                 fixed=None,
                 offset_lines=True,
                 line_settings=None,
                 background=None,
                 window_conf=None,
                 profile=False,
                 record=False,
                 **kwargs):
        """
        Create a window that will display a trimesh.Scene object
        in an OpenGL context via pyglet.

        Parameters
        ---------------
        scene : trimesh.scene.Scene
          Scene with geometry and transforms
        smooth : bool
          If True try to smooth shade things
        flags : dict
          If passed apply keys to self.view:
          ['cull', 'wireframe', etc]
        visible : bool
          Display window or not
        resolution : (2,) int
          Initial resolution of window
        start_loop : bool
          Call pyglet.app.run() at the end of init
        callback : function
          A function which can be called periodically to
          update things in the scene
        callback_period : float
          How often to call the callback, in seconds
        fixed : None or iterable
          List of keys in scene.geometry to skip view
          transform on to keep fixed relative to camera
        offset_lines : bool
          If True, will offset lines slightly so if drawn
          coplanar with mesh geometry they will be visible
        background : None or (4,) uint8
          Color for background
        window_conf : None, or gl.Config
          Passed to window init
        profile : bool
          If set will run a `pyinstrument` profile for
          every call to `on_draw` and print the output.
        record : bool
          If True, will save a list of `png` bytes to
          a list located in `scene.metadata['recording']`
        kwargs : dict
          Additional arguments to pass, including
          'background' for to set background color
        """
        self.scene = self._scene = scene

        self.callback = callback
        self.callback_period = callback_period
        self.scene._redraw = self._redraw
        self.offset_lines = bool(offset_lines)
        self.background = background
        # save initial camera transform
        self._initial_camera_transform = scene.camera_transform.copy()

        # a transform to offset lines slightly to avoid Z-fighting
        self._line_offset = translation_matrix(
            [0, 0, scene.scale / 1000 if self.offset_lines else 0])

        self.reset_view()
        self.batch = pyglet.graphics.Batch()
        self._smooth = smooth

        self._profile = bool(profile)
        if self._profile:
            from pyinstrument import Profiler
            self.Profiler = Profiler

        self._record = bool(record)
        if self._record:
            # will save bytes here
            self.scene.metadata['recording'] = []

        # store kwargs
        self.kwargs = kwargs

        # store a vertexlist for an axis marker
        self._axis = None
        # store a vertexlist for a grid display
        self._grid = None
        # store scene geometry as vertex lists
        self.vertex_list = {}
        # store geometry hashes
        self.vertex_list_hash = {}
        # store geometry rendering mode
        self.vertex_list_mode = {}
        # store meshes that don't rotate relative to viewer
        self.fixed = fixed
        # store a hidden (don't not display) node.
        self._nodes_hidden = set()
        # name : texture
        self.textures = {}

        # if resolution isn't defined set a default value
        if resolution is None:
            resolution = scene.camera.resolution
        else:
            scene.camera.resolution = resolution

        # set the default line settings to a fraction
        # of our resolution so the points aren't tiny
        scale = max(resolution)
        self.line_settings = {'point_size': scale / 200,
                              'line_width': scale / 400}
        # if we've been passed line settings override the default
        if line_settings is not None:
            self.line_settings.update(line_settings)

        # no window conf was passed so try to get the best looking one
        if window_conf is None:
            try:
                # try enabling antialiasing
                # if you have a graphics card this will probably work
                conf = gl.Config(sample_buffers=1,
                                 samples=4,
                                 depth_size=24,
                                 double_buffer=True)
                super(SceneViewer, self).__init__(config=conf,
                                                  visible=visible,
                                                  resizable=True,
                                                  width=resolution[0],
                                                  height=resolution[1],
                                                  caption=caption)
            except pyglet.window.NoSuchConfigException:
                conf = gl.Config(double_buffer=True)
                super(SceneViewer, self).__init__(config=conf,
                                                  resizable=True,
                                                  visible=visible,
                                                  width=resolution[0],
                                                  height=resolution[1],
                                                  caption=caption)
        else:
            # window config was manually passed
            super(SceneViewer, self).__init__(config=window_conf,
                                              resizable=True,
                                              visible=visible,
                                              width=resolution[0],
                                              height=resolution[1],
                                              caption=caption)

        # add scene geometry to viewer geometry
        self._update_vertex_list()

        # call after geometry is added
        self.init_gl()
        self.set_size(*resolution)
        if flags is not None:
            self.reset_view(flags=flags)
        self.update_flags()

        # someone has passed a callback to be called periodically
        if self.callback is not None:
            # if no callback period is specified set it to default
            if callback_period is None:
                # 30 times per second
                callback_period = 1.0 / 30.0
            # set up a do-nothing periodic task which will
            # trigger `self.on_draw` every `callback_period`
            # seconds if someone has passed a callback
            pyglet.clock.schedule_interval(lambda x: x,
                                           callback_period)
        if start_loop:
            pyglet.app.run()

    def _redraw(self):
        self.on_draw()

    def _update_vertex_list(self):
        # update vertex_list if needed
        for name, geom in self.scene.geometry.items():
            if geom.is_empty:
                continue
            if geometry_hash(geom) == self.vertex_list_hash.get(name):
                continue
            self.add_geometry(name=name,
                              geometry=geom,
                              smooth=bool(self._smooth))

    def _update_meshes(self):
        # call the callback if specified
        if self.callback is not None:
            self.callback(self.scene)
            self._update_vertex_list()
            self._update_perspective(self.width, self.height)

    def add_geometry(self, name, geometry, **kwargs):
        """
        Add a geometry to the viewer.

        Parameters
        --------------
        name : hashable
          Name that references geometry
        geometry : Trimesh, Path2D, Path3D, PointCloud
          Geometry to display in the viewer window
        kwargs **
          Passed to rendering.convert_to_vertexlist
        """
        try:
            # convert geometry to constructor args
            args = rendering.convert_to_vertexlist(geometry, **kwargs)
        except BaseException:
            util.log.warning('failed to add geometry `{}`'.format(name),
                             exc_info=True)
            return

        # create the indexed vertex list
        self.vertex_list[name] = self.batch.add_indexed(*args)
        # save the hash of the geometry
        self.vertex_list_hash[name] = geometry_hash(geometry)
        # save the rendering mode from the constructor args
        self.vertex_list_mode[name] = args[1]

        # get the visual if the element has it
        visual = getattr(geometry, 'visual', None)
        if hasattr(visual, 'uv') and hasattr(visual, 'material'):
            try:
                tex = rendering.material_to_texture(visual.material)
                if tex is not None:
                    self.textures[name] = tex
            except BaseException:
                util.log.warning('failed to load texture', exc_info=True)

    def cleanup_geometries(self):
        """
        Remove any stored vertex lists that no longer
        exist in the scene.
        """
        # shorthand to scene graph
        graph = self.scene.graph
        # which parts of the graph still have geometry
        geom_keep = set([graph[node][1] for
                         node in graph.nodes_geometry])
        # which geometries no longer need to be kept
        geom_delete = [geom for geom in self.vertex_list
                       if geom not in geom_keep]
        for geom in geom_delete:
            # remove stored vertex references
            self.vertex_list.pop(geom, None)
            self.vertex_list_hash.pop(geom, None)
            self.vertex_list_mode.pop(geom, None)
            self.textures.pop(geom, None)

    def unhide_geometry(self, node):
        """
        If a node is hidden remove the flag and show the
        geometry on the next draw.

        Parameters
        -------------
        node : str
          Node to display
        """
        self._nodes_hidden.discard(node)

    def hide_geometry(self, node):
        """
        Don't display the geometry contained at a node on
        the next draw.

        Parameters
        -------------
        node : str
          Node to not display
        """
        self._nodes_hidden.add(node)

    def reset_view(self, flags=None):
        """
        Set view to the default view.

        Parameters
        --------------
        flags : None or dict
          If any view key passed override the default
          e.g. {'cull': False}
        """
        self.view = {
            'cull': True,
            'axis': False,
            'grid': False,
            'fullscreen': False,
            'wireframe': False,
            'ball': Trackball(
                pose=self._initial_camera_transform,
                size=self.scene.camera.resolution,
                scale=self.scene.scale,
                target=self.scene.centroid)}
        try:
            # if any flags are passed override defaults
            if isinstance(flags, dict):
                for k, v in flags.items():
                    if k in self.view:
                        self.view[k] = v
                self.update_flags()
        except BaseException:
            pass

    def init_gl(self):
        """
        Perform the magic incantations to create an
        OpenGL scene using pyglet.
        """

        # if user passed a background color use it
        if self.background is None:
            # default background color is white
            background = np.ones(4)
        else:
            # convert to (4,) uint8 RGBA
            background = to_rgba(self.background)
            # convert to 0.0-1.0 float
            background = background.astype(np.float64) / 255.0

        self._gl_set_background(background)
        # use camera setting for depth
        self._gl_enable_depth(self.scene.camera)
        self._gl_enable_color_material()
        self._gl_enable_blending()
        self._gl_enable_smooth_lines(**self.line_settings)
        self._gl_enable_lighting(self.scene)

    @staticmethod
    def _gl_set_background(background):
        gl.glClearColor(*background)

    @staticmethod
    def _gl_unset_background():
        gl.glClearColor(*[0, 0, 0, 0])

    @staticmethod
    def _gl_enable_depth(camera):
        """
        Enable depth test in OpenGL using distances
        from `scene.camera`.
        """
        gl.glClearDepth(1.0)
        gl.glEnable(gl.GL_DEPTH_TEST)
        gl.glDepthFunc(gl.GL_LEQUAL)

        gl.glEnable(gl.GL_DEPTH_TEST)
        gl.glEnable(gl.GL_CULL_FACE)

    @staticmethod
    def _gl_enable_color_material():
        # do some openGL things
        gl.glColorMaterial(gl.GL_FRONT_AND_BACK,
                           gl.GL_AMBIENT_AND_DIFFUSE)
        gl.glEnable(gl.GL_COLOR_MATERIAL)
        gl.glShadeModel(gl.GL_SMOOTH)

        gl.glMaterialfv(gl.GL_FRONT,
                        gl.GL_AMBIENT,
                        rendering.vector_to_gl(
                            0.192250, 0.192250, 0.192250))
        gl.glMaterialfv(gl.GL_FRONT,
                        gl.GL_DIFFUSE,
                        rendering.vector_to_gl(
                            0.507540, 0.507540, 0.507540))
        gl.glMaterialfv(gl.GL_FRONT,
                        gl.GL_SPECULAR,
                        rendering.vector_to_gl(
                            .5082730, .5082730, .5082730))

        gl.glMaterialf(gl.GL_FRONT,
                       gl.GL_SHININESS,
                       .4 * 128.0)

    @staticmethod
    def _gl_enable_blending():
        # enable blending for transparency
        gl.glEnable(gl.GL_BLEND)
        gl.glBlendFunc(gl.GL_SRC_ALPHA,
                       gl.GL_ONE_MINUS_SRC_ALPHA)

    @staticmethod
    def _gl_enable_smooth_lines(line_width=4, point_size=4):
        # make the lines from Path3D objects less ugly
        gl.glEnable(gl.GL_LINE_SMOOTH)
        gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
        # set the width of lines to 4 pixels
        gl.glLineWidth(line_width)
        # set PointCloud markers to 4 pixels in size
        gl.glPointSize(point_size)

    @staticmethod
    def _gl_enable_lighting(scene):
        """
        Take the lights defined in scene.lights and
        apply them as openGL lights.
        """
        gl.glEnable(gl.GL_LIGHTING)
        # opengl only supports 7 lights?
        for i, light in enumerate(scene.lights[:7]):
            # the index of which light we have
            lightN = eval('gl.GL_LIGHT{}'.format(i))

            # get the transform for the light by name
            matrix = scene.graph.get(light.name)[0]

            # convert light object to glLightfv calls
            multiargs = rendering.light_to_gl(
                light=light,
                transform=matrix,
                lightN=lightN)

            # enable the light in question
            gl.glEnable(lightN)
            # run the glLightfv calls
            for args in multiargs:
                gl.glLightfv(*args)

    def toggle_culling(self):
        """
        Toggle back face culling.

        It is on by default but if you are dealing with
        non- watertight meshes you probably want to be able
        to see the back sides.
        """
        self.view['cull'] = not self.view['cull']
        self.update_flags()

    def toggle_wireframe(self):
        """
        Toggle wireframe mode

        Good for  looking inside meshes, off by default.
        """
        self.view['wireframe'] = not self.view['wireframe']
        self.update_flags()

    def toggle_fullscreen(self):
        """
        Toggle between fullscreen and windowed mode.
        """
        self.view['fullscreen'] = not self.view['fullscreen']
        self.update_flags()

    def toggle_axis(self):
        """
        Toggle a rendered XYZ/RGB axis marker:
        off, world frame, every frame
        """
        # cycle through three axis states
        states = [False, 'world', 'all', 'without_world']
        # the state after toggling
        index = (states.index(self.view['axis']) + 1) % len(states)
        # update state to next index
        self.view['axis'] = states[index]
        # perform gl actions
        self.update_flags()

    def toggle_grid(self):
        """
        Toggle a rendered grid.
        """
        # update state to next index
        self.view['grid'] = not self.view['grid']
        # perform gl actions
        self.update_flags()

    def update_flags(self):
        """
        Check the view flags, and call required GL functions.
        """
        # view mode, filled vs wirefrom
        if self.view['wireframe']:
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
        else:
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)

        # set fullscreen or windowed
        self.set_fullscreen(fullscreen=self.view['fullscreen'])

        # backface culling on or off
        if self.view['cull']:
            gl.glEnable(gl.GL_CULL_FACE)
        else:
            gl.glDisable(gl.GL_CULL_FACE)

        # case where we WANT an axis and NO vertexlist
        # is stored internally
        if self.view['axis'] and self._axis is None:
            from .. import creation
            # create an axis marker sized relative to the scene
            axis = creation.axis(origin_size=self.scene.scale / 100)
            # create ordered args for a vertex list
            args = rendering.mesh_to_vertexlist(axis)
            # store the axis as a reference
            self._axis = self.batch.add_indexed(*args)
        # case where we DON'T want an axis but a vertexlist
        # IS stored internally
        elif not self.view['axis'] and self._axis is not None:
            # remove the axis from the rendering batch
            self._axis.delete()
            # set the reference to None
            self._axis = None

        if self.view['grid'] and self._grid is None:
            try:
                # create a grid marker
                from ..path.creation import grid
                bounds = self.scene.bounds
                center = bounds.mean(axis=0)
                # set the grid to the lowest Z position
                # also offset by the scale to avoid interference
                center[2] = bounds[0][2] - (bounds[:, 2].ptp() / 100)
                # choose the side length by maximum XY length
                side = bounds.ptp(axis=0)[:2].max()
                # create an axis marker sized relative to the scene
                grid_mesh = grid(
                    side=side,
                    count=4,
                    transform=translation_matrix(center))
                # convert the path to vertexlist args
                args = rendering.convert_to_vertexlist(grid_mesh)
                # create ordered args for a vertex list
                self._grid = self.batch.add_indexed(*args)
            except BaseException:
                util.log.warning(
                    'failed to create grid!', exc_info=True)
        elif not self.view['grid'] and self._grid is not None:
            self._grid.delete()
            self._grid = None

    def _update_perspective(self, width, height):
        try:
            # for high DPI screens viewport size
            # will be different then the passed size
            width, height = self.get_viewport_size()
        except BaseException:
            # older versions of pyglet may not have this
            pass

        # set the new viewport size
        gl.glViewport(0, 0, width, height)
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()

        # get field of view and Z range from camera
        camera = self.scene.camera

        # set perspective from camera data
        gl.gluPerspective(camera.fov[1],
                          width / float(height),
                          camera.z_near,
                          camera.z_far)
        gl.glMatrixMode(gl.GL_MODELVIEW)

        return width, height

    def on_resize(self, width, height):
        """
        Handle resized windows.
        """
        width, height = self._update_perspective(width, height)
        self.scene.camera.resolution = (width, height)
        self.view['ball'].resize(self.scene.camera.resolution)
        self.scene.camera_transform = self.view['ball'].pose

    def on_mouse_press(self, x, y, buttons, modifiers):
        """
        Set the start point of the drag.
        """
        self.view['ball'].set_state(Trackball.STATE_ROTATE)
        if (buttons == pyglet.window.mouse.LEFT):
            ctrl = (modifiers & pyglet.window.key.MOD_CTRL)
            shift = (modifiers & pyglet.window.key.MOD_SHIFT)
            if (ctrl and shift):
                self.view['ball'].set_state(Trackball.STATE_ZOOM)
            elif shift:
                self.view['ball'].set_state(Trackball.STATE_ROLL)
            elif ctrl:
                self.view['ball'].set_state(Trackball.STATE_PAN)
        elif (buttons == pyglet.window.mouse.MIDDLE):
            self.view['ball'].set_state(Trackball.STATE_PAN)
        elif (buttons == pyglet.window.mouse.RIGHT):
            self.view['ball'].set_state(Trackball.STATE_ZOOM)

        self.view['ball'].down(np.array([x, y]))
        self.scene.camera_transform = self.view['ball'].pose

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        """
        Pan or rotate the view.
        """
        self.view['ball'].drag(np.array([x, y]))
        self.scene.camera_transform = self.view['ball'].pose

    def on_mouse_scroll(self, x, y, dx, dy):
        """
        Zoom the view.
        """
        self.view['ball'].scroll(dy)
        self.scene.camera_transform = self.view['ball'].pose

    def on_key_press(self, symbol, modifiers):
        """
        Call appropriate functions given key presses.
        """
        magnitude = 10
        if symbol == pyglet.window.key.W:
            self.toggle_wireframe()
        elif symbol == pyglet.window.key.Z:
            self.reset_view()
        elif symbol == pyglet.window.key.C:
            self.toggle_culling()
        elif symbol == pyglet.window.key.A:
            self.toggle_axis()
        elif symbol == pyglet.window.key.G:
            self.toggle_grid()
        elif symbol == pyglet.window.key.Q:
            self.on_close()
        elif symbol == pyglet.window.key.M:
            self.maximize()
        elif symbol == pyglet.window.key.F:
            self.toggle_fullscreen()

        if symbol in [
                pyglet.window.key.LEFT,
                pyglet.window.key.RIGHT,
                pyglet.window.key.DOWN,
                pyglet.window.key.UP]:
            self.view['ball'].down([0, 0])
            if symbol == pyglet.window.key.LEFT:
                self.view['ball'].drag([-magnitude, 0])
            elif symbol == pyglet.window.key.RIGHT:
                self.view['ball'].drag([magnitude, 0])
            elif symbol == pyglet.window.key.DOWN:
                self.view['ball'].drag([0, -magnitude])
            elif symbol == pyglet.window.key.UP:
                self.view['ball'].drag([0, magnitude])
            self.scene.camera_transform = self.view['ball'].pose

    def on_draw(self):
        """
        Run the actual draw calls.
        """

        if self._profile:
            profiler = self.Profiler()
            profiler.start()

        self._update_meshes()
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
        gl.glLoadIdentity()

        # pull the new camera transform from the scene
        transform_camera = np.linalg.inv(self.scene.camera_transform)

        # apply the camera transform to the matrix stack
        gl.glMultMatrixf(rendering.matrix_to_gl(transform_camera))

        # we want to render fully opaque objects first,
        # followed by objects which have transparency
        node_names = collections.deque(self.scene.graph.nodes_geometry)
        # how many nodes did we start with
        count_original = len(node_names)
        count = -1

        # if we are rendering an axis marker at the world
        if self._axis and not self.view['axis'] == 'without_world':
            # we stored it as a vertex list
            self._axis.draw(mode=gl.GL_TRIANGLES)
        if self._grid:
            self._grid.draw(mode=gl.GL_LINES)

        # save a reference outside of the loop
        geometry = self.scene.geometry
        graph = self.scene.graph

        while len(node_names) > 0:
            count += 1
            current_node = node_names.popleft()

            if current_node in self._nodes_hidden:
                continue

            # get the transform from world to geometry and mesh name
            transform, geometry_name = graph.get(current_node)

            # if no geometry at this frame continue without rendering
            if geometry_name is None or geometry_name not in self.vertex_list_mode:
                continue

            # if a geometry is marked as fixed apply the inverse view transform
            if self.fixed is not None and geometry_name in self.fixed:
                # remove altered camera transform from fixed geometry
                transform_fix = np.linalg.inv(
                    np.dot(self._initial_camera_transform, transform_camera))
                # apply the transform so the fixed geometry doesn't move
                transform = np.dot(transform, transform_fix)

            # get a reference to the mesh so we can check transparency
            mesh = geometry[geometry_name]
            if mesh.is_empty:
                continue

            # get the GL mode of the current geometry
            mode = self.vertex_list_mode[geometry_name]

            # if you draw a coplanar line with a triangle it will z-fight
            # the best way to do this is probably a shader but this works fine
            if mode == gl.GL_LINES:
                # apply the offset in camera space
                transform = util.multi_dot([
                    transform,
                    np.linalg.inv(transform_camera),
                    self._line_offset,
                    transform_camera])

            # add a new matrix to the model stack
            gl.glPushMatrix()
            # transform by the nodes transform
            gl.glMultMatrixf(rendering.matrix_to_gl(transform))

            # draw an axis marker for each mesh frame
            if self.view['axis'] == 'all':
                self._axis.draw(mode=gl.GL_TRIANGLES)
            elif self.view['axis'] == 'without_world':
                if not util.allclose(transform, np.eye(4), atol=1e-5):
                    self._axis.draw(mode=gl.GL_TRIANGLES)

            # transparent things must be drawn last
            if (hasattr(mesh, 'visual') and
                hasattr(mesh.visual, 'transparency')
                    and mesh.visual.transparency):
                # put the current item onto the back of the queue
                if count < count_original:
                    # add the node to be drawn last
                    node_names.append(current_node)
                    # pop the matrix stack for now
                    gl.glPopMatrix()
                    # come back to this mesh later
                    continue

            # if we have texture enable the target texture
            texture = None
            if geometry_name in self.textures:
                texture = self.textures[geometry_name]
                gl.glEnable(texture.target)
                gl.glBindTexture(texture.target, texture.id)

            # draw the mesh with its transform applied
            self.vertex_list[geometry_name].draw(mode=mode)
            # pop the matrix stack as we drew what we needed to draw
            gl.glPopMatrix()

            # disable texture after using
            if texture is not None:
                gl.glDisable(texture.target)

        if self._profile:
            profiler.stop()
            util.log.debug(profiler.output_text(unicode=True, color=True))

    def flip(self):
        super(SceneViewer, self).flip()
        if self._record:
            # will save a PNG-encoded bytes
            img = self.save_image(util.BytesIO())
            # seek start of file-like object
            img.seek(0)
            # save the bytes from the file object
            self.scene.metadata['recording'].append(img.read())

    def save_image(self, file_obj):
        """
        Save the current color buffer to a file object
        in PNG format.

        Parameters
        -------------
        file_obj: file name, or file- like object
        """
        manager = pyglet.image.get_buffer_manager()
        colorbuffer = manager.get_color_buffer()
        # if passed a string save by name
        if hasattr(file_obj, 'write'):
            colorbuffer.save(file=file_obj)
        else:
            colorbuffer.save(filename=file_obj)
        return file_obj


def geometry_hash(geometry):
    """
    Get a hash for a geometry object

    Parameters
    ------------
    geometry : object

    Returns
    ------------
    hash : str
    """
    h = str(hash(geometry))
    if hasattr(geometry, 'visual'):
        # if visual properties are defined
        h += str(hash(geometry.visual))

    return h


def render_scene(scene,
                 resolution=None,
                 visible=True,
                 **kwargs):
    """
    Render a preview of a scene to a PNG. Note that
    whether this works or not highly variable based on
    platform and graphics driver.

    Parameters
    ------------
    scene : trimesh.Scene
      Geometry to be rendered
    resolution : (2,) int or None
      Resolution in pixels or set from scene.camera
    visible : bool
      Show a window during rendering. Note that MANY
      platforms refuse to render with hidden windows
      and will likely return a blank image; this is a
      platform issue and cannot be fixed in Python.
    kwargs : **
      Passed to SceneViewer

    Returns
    ---------
    render : bytes
      Image in PNG format
    """
    window = SceneViewer(
        scene, start_loop=False, visible=visible,
        resolution=resolution, **kwargs)

    from ..util import BytesIO

    # need to run loop twice to display anything
    for save in [False, False, True]:
        pyglet.clock.tick()
        window.switch_to()
        window.dispatch_events()
        window.dispatch_event('on_draw')
        window.flip()
        if save:
            # save the color buffer data to memory
            file_obj = BytesIO()
            window.save_image(file_obj)
            file_obj.seek(0)
            render = file_obj.read()
    window.close()

    return render


# trimesh/voxel/__init__.py

from .base import VoxelGrid

__all__ = ['VoxelGrid']


# trimesh/voxel/base.py

"""
voxel.py
-----------

Convert meshes to a simple voxel data structure and back again.
"""
import numpy as np

from . import ops
from . import transforms
from . import morphology

from .encoding import Encoding, DenseEncoding
from .. import util
from .. import caching
from .. import bounds as bounds_module
from .. import transformations as tr

from ..parent import Geometry
from ..constants import log

from ..exchange.binvox import export_binvox


class VoxelGrid(Geometry):
    """
    Store 3D voxels.
    """

    def __init__(self, encoding, transform=None, metadata=None):
        if transform is None:
            transform = np.eye(4)
        if isinstance(encoding, np.ndarray):
            encoding = DenseEncoding(encoding.astype(bool))
        if encoding.dtype != bool:
            raise ValueError('encoding must have dtype bool')
        self._data = caching.DataStore()
        self.encoding = encoding
        self._data['transform'] = transforms.Transform(transform)
        self._cache = caching.Cache(
            id_function=self._data.__hash__)

        self.metadata = dict()
        # update the mesh metadata with passed metadata
        if isinstance(metadata, dict):
            self.metadata.update(metadata)
        elif metadata is not None:
            raise ValueError(
                'metadata should be a dict or None, got %s' % str(metadata))

    def crc(self):
        util.log.warning(
            '`geometry.crc()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def hash(self):
        util.log.warning(
            '`geometry.hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def __hash__(self):
        """
        Get the hash of the current transformation matrix.

        Returns
        ------------
        hash : str
          Hash of transformation matrix
        """
        return self._data.__hash__()

    @property
    def encoding(self):
        """
        `Encoding` object providing the occupancy grid.

        See `trimesh.voxel.encoding` for implementations.
        """
        return self._data['encoding']

    @encoding.setter
    def encoding(self, encoding):
        if isinstance(encoding, np.ndarray):
            encoding = DenseEncoding(encoding)
        elif not isinstance(encoding, Encoding):
            raise ValueError(
                'encoding must be an Encoding, got %s' % str(encoding))
        if len(encoding.shape) != 3:
            raise ValueError(
                'encoding must be rank 3, got shape %s' % str(encoding.shape))
        if encoding.dtype != bool:
            raise ValueError(
                'encoding must be binary, got %s' % encoding.dtype)
        self._data['encoding'] = encoding

    @property
    def _transform(self):
        return self._data['transform']

    @property
    def transform(self):
        """4x4 homogeneous transformation matrix."""
        return self._transform.matrix

    @transform.setter
    def transform(self, matrix):
        """4x4 homogeneous transformation matrix."""
        self._transform.matrix = matrix

    @property
    def translation(self):
        """Location of voxel at [0, 0, 0]."""
        return self._transform.translation

    @property
    def origin(self):
        """Deprecated. Use `self.translation`."""
        # DEPRECATED. Use translation instead
        return self.translation

    @property
    def scale(self):
        """
        3-element float representing per-axis scale.

        Raises a `RuntimeError` if `self.transform` has rotation or
        shear components.
        """
        return self._transform.scale

    @property
    def pitch(self):
        """
        Uniform scaling factor representing the side length of
        each voxel.

        Returns
        -----------
        pitch : float
          Pitch of the voxels.

        Raises
        ------------
        `RuntimeError`
          If `self.transformation` has rotation or shear
          components of has non-uniform scaling.
        """
        return self._transform.pitch

    @property
    def element_volume(self):
        return self._transform.unit_volume

    def apply_transform(self, matrix):
        self._transform.apply_transform(matrix)
        return self

    def strip(self):
        """
        Mutate self by stripping leading/trailing planes of zeros.

        Returns
        --------
        self after mutation occurs in-place
        """
        encoding, padding = self.encoding.stripped
        self.encoding = encoding
        self._transform.matrix[:3, 3] = self.indices_to_points(padding[:, 0])
        return self

    @caching.cache_decorator
    def bounds(self):
        indices = self.sparse_indices
        # get all 8 corners of the AABB
        corners = bounds_module.corners(
            [indices.min(axis=0) - 0.5,
             indices.max(axis=0) + 0.5])
        # transform these corners to a new frame
        corners = self._transform.transform_points(corners)
        # get the AABB of corners in-frame
        bounds = np.array([corners.min(axis=0), corners.max(axis=0)])
        bounds.flags.writeable = False
        return bounds

    @caching.cache_decorator
    def extents(self):
        bounds = self.bounds
        extents = bounds[1] - bounds[0]
        extents.flags.writeable = False
        return extents

    @caching.cache_decorator
    def is_empty(self):
        return self.encoding.is_empty

    @property
    def shape(self):
        """3-tuple of ints denoting shape of occupancy grid."""
        return self.encoding.shape

    @caching.cache_decorator
    def filled_count(self):
        """int, number of occupied voxels in the grid."""
        return self.encoding.sum.item()

    def is_filled(self, point):
        """
        Query points to see if the voxel cells they lie in are
        filled or not.

        Parameters
        ----------
        point : (n, 3) float
          Points in space

        Returns
        ---------
        is_filled : (n,) bool
          Is cell occupied or not for each point
        """
        point = np.asanyarray(point)
        indices = self.points_to_indices(point)
        in_range = np.logical_and(
            np.all(indices < np.array(self.shape), axis=-1),
            np.all(indices >= 0, axis=-1))

        is_filled = np.zeros_like(in_range)
        is_filled[in_range] = self.encoding.gather_nd(indices[in_range])
        return is_filled

    def fill(self, method='holes', **kwargs):
        """
        Mutates self by filling in the encoding according
        to `morphology.fill`.

        Parameters
        ----------
        method : hashable
          Implementation key, one of
          `trimesh.voxel.morphology.fill.fillers` keys
        **kwargs : dict
          Additional kwargs passed through to
          the keyed implementation.

        Returns
        ----------
        self : VoxelGrid
          After replacing encoding with a filled version.
        """
        self.encoding = morphology.fill(
            self.encoding, method=method, **kwargs)
        return self

    def hollow(self):
        """
        Mutates self by removing internal voxels
        leaving only surface elements.

        Surviving elements are those in encoding that are
        adjacent to an empty voxel where adjacency is
        controlled by `structure`.

        Returns
        ----------
        self : VoxelGrid
          After replacing encoding with a surface version.
        """
        self.encoding = morphology.surface(self.encoding)
        return self

    @caching.cache_decorator
    def marching_cubes(self):
        """
        A marching cubes Trimesh representation of the voxels.

        No effort was made to clean or smooth the result in any way;
        it is merely the result of applying the scikit-image
        measure.marching_cubes function to self.encoding.dense.

        Returns
        ---------
        meshed : trimesh.Trimesh
          Representing the current voxel
          object as returned by marching cubes algorithm.
        """
        return ops.matrix_to_marching_cubes(matrix=self.matrix)

    @property
    def matrix(self):
        """
        Return a DENSE matrix of the current voxel encoding.

        Returns
        -------------
        dense : (a, b, c) bool
          Numpy array of dense matrix
          Shortcut to voxel.encoding.dense
        """
        return self.encoding.dense

    @caching.cache_decorator
    def volume(self):
        """
        What is the volume of the filled cells in the current
        voxel object.

        Returns
        ---------
        volume : float
          Volume of filled cells.
        """
        return self.filled_count * self.element_volume

    @caching.cache_decorator
    def points(self):
        """
        The center of each filled cell as a list of points.

        Returns
        ----------
        points : (self.filled, 3) float
          Points in space.
        """
        return self._transform.transform_points(
            self.sparse_indices.astype(float))

    @property
    def sparse_indices(self):
        """(n, 3) int array of sparse indices of occupied voxels."""
        return self.encoding.sparse_indices

    def as_boxes(self, colors=None, **kwargs):
        """
        A rough Trimesh representation of the voxels with a box
        for each filled voxel.

        Parameters
        ----------
        colors : None, (3,) or (4,) float or uint8
          (X, Y, Z, 3) or (X, Y, Z, 4) float or uint8
          Where matrix.shape == (X, Y, Z)

        Returns
        ---------
        mesh : trimesh.Trimesh
          Mesh with one box per filled cell.
        """

        if colors is not None:
            colors = np.asanyarray(colors)
            if colors.ndim == 4:
                encoding = self.encoding
                if colors.shape[:3] == encoding.shape:
                    # TODO jackd: more efficient implementation?
                    # encoding.as_mask?
                    colors = colors[encoding.dense]
                else:
                    log.warning('colors incorrect shape!')
                    colors = None
            elif colors.shape not in ((3,), (4,)):
                log.warning('colors incorrect shape!')
                colors = None

        mesh = ops.multibox(
            centers=self.sparse_indices.astype(float), colors=colors)

        mesh = mesh.apply_transform(self.transform)
        return mesh

    def points_to_indices(self, points):
        """
        Convert points to indices in the matrix array.

        Parameters
        ----------
        points: (n, 3) float, point in space

        Returns
        ---------
        indices: (n, 3) int array of indices into self.encoding
        """
        points = self._transform.inverse_transform_points(points)
        return np.round(points).astype(int)

    def indices_to_points(self, indices):
        return self._transform.transform_points(indices.astype(float))

    def show(self, *args, **kwargs):
        """
        Convert the current set of voxels into a trimesh for visualization
        and show that via its built- in preview method.
        """
        return self.as_boxes(kwargs.pop(
            'colors', None)).show(*args, **kwargs)

    def copy(self):
        return VoxelGrid(
            self.encoding.copy(),
            self._transform.matrix.copy())

    def export(self, file_obj=None, file_type=None, **kwargs):
        """
        Export the current VoxelGrid.

        Parameters
        ------------
        file_obj : file-like or str
          File or file-name to export to.
        file_type : None or str
          Only 'binvox' currently supported.

        Returns
        ---------
        export : bytes
          Value of export.
        """
        if isinstance(file_obj, str) and file_type is None:
            file_type = util.split_extension(file_obj).lower()

        if file_type != 'binvox':
            raise ValueError('only binvox exports supported!')

        exported = export_binvox(self, **kwargs)
        if hasattr(file_obj, 'write'):
            file_obj.write(exported)
        elif isinstance(file_obj, str):
            with open(file_obj, 'wb') as f:
                f.write(exported)
        return exported

    def revoxelized(self, shape):
        """
        Create a new VoxelGrid without rotations, reflections
        or shearing.

        Parameters
        ----------
        shape : (3, int)
          The shape of the returned VoxelGrid.

        Returns
        ----------
        vox : VoxelGrid
          Of the given shape with possibly non-uniform
          scale and translation transformation matrix.
        """
        shape = tuple(shape)
        bounds = self.bounds.copy()
        extents = self.extents
        points = util.grid_linspace(
            bounds, shape).reshape(shape + (3,))
        dense = self.is_filled(points)
        scale = extents / np.asanyarray(shape)
        translate = bounds[0]
        return VoxelGrid(
            dense,
            transform=tr.scale_and_translate(scale, translate))

    def __add__(self, other):
        raise NotImplementedError("TODO : implement voxel concatenation")


# trimesh/voxel/creation.py

import numpy as np

from ..constants import log_time
from .. import remesh
from .. import grouping
from .. import util
from .. import transformations as tr

from . import base
from . import encoding as enc


@log_time
def voxelize_subdivide(mesh,
                       pitch,
                       max_iter=10,
                       edge_factor=2.0):
    """
    Voxelize a surface by subdividing a mesh until every edge is
    shorter than: (pitch / edge_factor)

    Parameters
    -----------
    mesh : trimesh.Trimesh
      Source mesh
    pitch : float
      Side length of a single voxel cube
    max_iter : int
      Cap maximum subdivisions or None for no limit.
    edge_factor : float
      Proportion of pitch maximum edge length.

    Returns
    -----------
    VoxelGrid instance representing the voxelized mesh.
    """
    max_edge = pitch / edge_factor

    if max_iter is None:
        longest_edge = np.linalg.norm(
            mesh.vertices[mesh.edges[:, 0]] -
            mesh.vertices[mesh.edges[:, 1]],
            axis=1).max()
        max_iter = max(int(np.ceil(np.log2(
            longest_edge / max_edge))), 0)

    # get the same mesh sudivided so every edge is shorter
    # than a factor of our pitch
    v, f, idx = remesh.subdivide_to_size(mesh.vertices,
                                         mesh.faces,
                                         max_edge=max_edge,
                                         max_iter=max_iter,
                                         return_index=True)

    # convert the vertices to their voxel grid position
    hit = v / pitch

    # Provided edge_factor > 1 and max_iter is large enough, this is
    # sufficient to preserve 6-connectivity at the level of voxels.
    hit = np.round(hit).astype(int)

    # remove duplicates
    unique, inverse = grouping.unique_rows(hit)

    # get the voxel centers in model space
    occupied_index = hit[unique]

    origin_index = occupied_index.min(axis=0)
    origin_position = origin_index * pitch

    return base.VoxelGrid(
        enc.SparseBinaryEncoding(occupied_index - origin_index),
        transform=tr.scale_and_translate(
            scale=pitch, translate=origin_position))


def local_voxelize(mesh,
                   point,
                   pitch,
                   radius,
                   fill=True,
                   **kwargs):
    """
    Voxelize a mesh in the region of a cube around a point. When fill=True,
    uses proximity.contains to fill the resulting voxels so may be meaningless
    for non-watertight meshes. Useful to reduce memory cost for small values of
    pitch as opposed to global voxelization.

    Parameters
    -----------
    mesh : trimesh.Trimesh
      Source geometry
    point : (3, ) float
      Point in space to voxelize around
    pitch :  float
      Side length of a single voxel cube
    radius : int
      Number of voxel cubes to return in each direction.
    kwargs : parameters to pass to voxelize_subdivide

    Returns
    -----------
    voxels : VoxelGrid instance with resolution (m, m, m) where m=2*radius+1
        or None if the volume is empty
    """
    from scipy import ndimage

    # make sure point is correct type/shape
    point = np.asanyarray(point, dtype=np.float64).reshape(3)
    # this is a gotcha- radius sounds a lot like it should be in
    # float model space, not int voxel space so check
    if not isinstance(radius, int):
        raise ValueError('radius needs to be an integer number of cubes!')

    # Bounds of region
    bounds = np.concatenate((point - (radius + 0.5) * pitch,
                             point + (radius + 0.5) * pitch))

    # faces that intersect axis aligned bounding box
    faces = list(mesh.triangles_tree.intersection(bounds))

    # didn't hit anything so exit
    if len(faces) == 0:
        return None

    local = mesh.submesh([[f] for f in faces], append=True)

    # Translate mesh so point is at 0,0,0
    local.apply_translation(-point)

    # sparse, origin = voxelize_subdivide(local, pitch, **kwargs)
    vox = voxelize_subdivide(local, pitch, **kwargs)
    origin = vox.transform[:3, 3]
    matrix = vox.encoding.dense

    # Find voxel index for point
    center = np.round(-origin / pitch).astype(np.int64)

    # pad matrix if necessary
    prepad = np.maximum(radius - center, 0)
    postpad = np.maximum(center + radius + 1 - matrix.shape, 0)

    matrix = np.pad(matrix, np.stack((prepad, postpad), axis=-1),
                    mode='constant')
    center += prepad

    # Extract voxels within the bounding box
    voxels = matrix[center[0] - radius:center[0] + radius + 1,
                    center[1] - radius:center[1] + radius + 1,
                    center[2] - radius:center[2] + radius + 1]
    local_origin = point - radius * pitch  # origin of local voxels

    # Fill internal regions
    if fill:
        regions, n = ndimage.label(~voxels)
        distance = ndimage.distance_transform_cdt(~voxels)
        representatives = [
            np.unravel_index((distance * (regions == i)).argmax(),
                             distance.shape) for i in range(1, n + 1)]
        contains = mesh.contains(
            np.asarray(representatives) *
            pitch +
            local_origin)

        where = np.where(contains)[0] + 1
        # use in1d vs isin for older numpy versions
        internal = np.in1d(regions.flatten(), where).reshape(regions.shape)

        voxels = np.logical_or(voxels, internal)

    return base.VoxelGrid(voxels, tr.translation_matrix(local_origin))


@log_time
def voxelize_ray(mesh,
                 pitch,
                 per_cell=None):
    """
    Voxelize a mesh using ray queries.

    Parameters
    -------------
    mesh     : Trimesh object
                 Mesh to be voxelized
    pitch    : float
                 Length of voxel cube
    per_cell : (2,) int
                 How many ray queries to make per cell

    Returns
    -------------
    VoxelGrid instance representing the voxelized mesh.
    """
    if per_cell is None:
        # how many rays per cell
        per_cell = np.array([2, 2], dtype=np.int64)
    else:
        per_cell = np.array(per_cell, dtype=np.int64).reshape(2)

    # edge length of cube voxels
    pitch = float(pitch)

    # create the ray origins in a grid
    bounds = mesh.bounds[:, :2].copy()
    # offset start so we get the requested number per cell
    bounds[0] += pitch / (1.0 + per_cell)
    # offset end so arange doesn't short us
    bounds[1] += pitch
    # on X we are doing multiple rays per voxel step
    step = pitch / per_cell
    # 2D grid
    ray_ori = util.grid_arange(bounds, step=step)
    # a Z position below the mesh
    z = np.ones(len(ray_ori)) * (mesh.bounds[0][2] - pitch)
    ray_ori = np.column_stack((ray_ori, z))
    # all rays are along positive Z
    ray_dir = np.ones_like(ray_ori) * [0, 0, 1]

    # if you have pyembree this should be decently fast
    hits = mesh.ray.intersects_location(ray_ori, ray_dir)[0]

    # just convert hit locations to integer positions
    voxels = np.round(hits / pitch).astype(np.int64)

    # offset voxels by min, so matrix isn't huge
    origin_index = voxels.min(axis=0)
    voxels -= origin_index
    encoding = enc.SparseBinaryEncoding(voxels)
    origin_position = origin_index * pitch
    return base.VoxelGrid(
        encoding,
        tr.scale_and_translate(scale=pitch, translate=origin_position))


@log_time
def voxelize_binvox(mesh,
                    pitch=None,
                    dimension=None,
                    bounds=None,
                    **binvoxer_kwargs):
    """
    Voxelize via binvox tool.

    Parameters
    --------------
    mesh : trimesh.Trimesh
      Mesh to voxelize
    pitch : float
      Side length of each voxel. Ignored if dimension is provided
    dimension: int
      Number of voxels along each dimension. If not provided, this is
        calculated based on pitch and bounds/mesh extents
    bounds: (2, 3) float
      min/max values of the returned `VoxelGrid` in each instance. Uses
      `mesh.bounds` if not provided.
    **binvoxer_kwargs:
      Passed to `trimesh.exchange.binvox.Binvoxer`.
      Should not contain `bounding_box` if bounds is not None.

    Returns
    --------------
    `VoxelGrid` instance

    Raises
    --------------
    `ValueError` if `bounds is not None and 'bounding_box' in binvoxer_kwargs`.
    """
    from trimesh.exchange import binvox

    if dimension is None:
        # pitch must be provided
        if bounds is None:
            extents = mesh.extents
        else:
            mins, maxs = bounds
            extents = maxs - mins
        dimension = int(np.ceil(np.max(extents) / pitch))
    if bounds is not None:
        if 'bounding_box' in binvoxer_kwargs:
            raise ValueError('Cannot provide both bounds and bounding_box')
        binvoxer_kwargs['bounding_box'] = np.asanyarray(bounds).flatten()

    binvoxer = binvox.Binvoxer(dimension=dimension, **binvoxer_kwargs)
    return binvox.voxelize_mesh(mesh, binvoxer)


voxelizers = util.FunctionRegistry(
    ray=voxelize_ray,
    subdivide=voxelize_subdivide,
    binvox=voxelize_binvox)


def voxelize(mesh, pitch, method='subdivide', **kwargs):
    """
    Voxelize the given mesh using the specified implementation.

    See `voxelizers` for available implementations or to add your own, e.g. via
    `voxelizers['custom_key'] = custom_fn`.

    `custom_fn` should have signature `(mesh, pitch, **kwargs) -> VoxelGrid`
    and should not modify encoding.

    Parameters
    --------------
    mesh: Trimesh object (left unchanged).
    pitch: float, side length of each voxel.
    method: implementation method. Must be in `fillers`.
    **kwargs: additional kwargs passed to the specified implementation.

    Returns
    --------------
    A VoxelGrid instance.
    """
    return voxelizers(method, mesh=mesh, pitch=pitch, **kwargs)


# trimesh/voxel/encoding.py

"""OO interfaces to encodings for ND arrays which caching."""
import numpy as np

import abc
from ..util import ABC, log

from . import runlength
from .. import caching

try:
    from scipy import sparse as sp
except BaseException as E:
    from ..exceptions import ExceptionWrapper
    sp = ExceptionWrapper(E)


def _empty_stripped(shape):
    num_dims = len(shape)
    encoding = DenseEncoding(
        np.empty(shape=(0,) * num_dims, dtype=bool))
    padding = np.zeros(shape=(num_dims, 2), dtype=int)
    padding[:, 1] = shape
    return encoding, padding


class Encoding(ABC):
    """
    Base class for objects that implement a specific subset of of ndarray ops.

    This presents a unified interface for various different ways of encoding
    conceptually dense arrays and to interoperate between them.

    Example implementations are ND sparse arrays, run length encoded arrays
    and dense encodings (wrappers around np.ndarrays).
    """

    def __init__(self, data):
        self._data = data
        self._cache = caching.Cache(
            id_function=self._data.__hash__)

    @abc.abstractproperty
    def dtype(self):
        pass

    @abc.abstractproperty
    def shape(self):
        pass

    @abc.abstractproperty
    def sum(self):
        pass

    @abc.abstractproperty
    def size(self):
        pass

    @abc.abstractproperty
    def sparse_indices(self):
        pass

    @abc.abstractproperty
    def sparse_values(self):
        pass

    @abc.abstractproperty
    def dense(self):
        pass

    @abc.abstractmethod
    def gather_nd(self, indices):
        pass

    @abc.abstractmethod
    def mask(self, mask):
        pass

    @abc.abstractmethod
    def get_value(self, index):
        pass

    @abc.abstractmethod
    def copy(self):
        pass

    @property
    def is_empty(self):
        return self.sparse_indices[self.sparse_values != 0].size == 0

    @caching.cache_decorator
    def stripped(self):
        """
        Get encoding with all zeros stripped from the start and end
        of each axis.

        Returns
        ------------
        encoding: ?
        padding : (n, 2) int
          Padding at the start and end that was stripped
        """
        if self.is_empty:
            return _empty_stripped(self.shape)
        dense = self.dense
        shape = dense.shape
        ndims = len(shape)
        padding = []
        slices = []
        for dim, size in enumerate(shape):
            axis = tuple(range(dim)) + tuple(range(dim + 1, ndims))
            filled = np.any(dense, axis=axis)
            indices, = np.nonzero(filled)
            lower = indices.min()
            upper = indices.max() + 1
            padding.append([lower, size - upper])
            slices.append(slice(lower, upper))
        return DenseEncoding(dense[tuple(slices)]), np.array(padding, int)

    def _flip(self, axes):
        return FlippedEncoding(self, axes)

    def crc(self):
        log.warning(
            '`geometry.crc()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def hash(self):
        log.warning(
            '`geometry.hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def __hash__(self):
        """
        Get the hash of the current transformation matrix.

        Returns
        ------------
        hash : str
          Hash of transformation matrix
        """
        return self._data.__hash__()

    @property
    def ndims(self):
        return len(self.shape)

    def reshape(self, shape):
        return self.flat if len(shape) == 1 else ShapedEncoding(self, shape)

    @property
    def flat(self):
        return FlattenedEncoding(self)

    def flip(self, axis=0):
        return _flipped(self, axis)

    @property
    def sparse_components(self):
        return self.sparse_indices, self.sparse_values

    @property
    def data(self):
        return self._data

    def run_length_data(self, dtype=np.int64):
        if self.ndims != 1:
            raise ValueError(
                '`run_length_data` only valid for flat encodings')
        return runlength.dense_to_rle(self.dense, dtype=dtype)

    def binary_run_length_data(self, dtype=np.int64):
        if self.ndims != 1:
            raise ValueError(
                '`run_length_data` only valid for flat encodings')
        return runlength.dense_to_brle(self.dense, dtype=dtype)

    def transpose(self, perm):
        return _transposed(self, perm)

    def _transpose(self, perm):
        return TransposedEncoding(self, perm)

    @property
    def mutable(self):
        return self._data.mutable

    @mutable.setter
    def mutable(self, value):
        self._data.mutable = value


class DenseEncoding(Encoding):
    """Simple `Encoding` implementation based on a numpy ndarray."""

    def __init__(self, data):
        if not isinstance(data, caching.TrackedArray):
            if not isinstance(data, np.ndarray):
                raise ValueError('DenseEncoding data must be a numpy array')
            data = caching.tracked_array(data)
        super(DenseEncoding, self).__init__(data=data)

    @property
    def dtype(self):
        return self._data.dtype

    @property
    def shape(self):
        return self._data.shape

    @caching.cache_decorator
    def sum(self):
        return self._data.sum()

    @caching.cache_decorator
    def is_empty(self):
        return not np.any(self._data)

    @property
    def size(self):
        return self._data.size

    @property
    def sparse_components(self):
        indices = self.sparse_indices
        values = self.gather(indices)
        return indices, values

    @caching.cache_decorator
    def sparse_indices(self):
        return np.column_stack(np.where(self._data))

    @caching.cache_decorator
    def sparse_values(self):
        return self.sparse_components[1]

    def _flip(self, axes):
        dense = self.dense
        for a in axes:
            dense = np.flip(dense, a)
        return DenseEncoding(dense)

    @property
    def dense(self):
        return self._data

    def gather(self, indices):
        return self._data[indices]

    def gather_nd(self, indices):
        return self._data[tuple(indices.T)]

    def mask(self, mask):
        return self._data[mask if isinstance(mask, np.ndarray) else mask.dense]

    def get_value(self, index):
        return self._data[tuple(index)]

    def reshape(self, shape):
        return DenseEncoding(self._data.reshape(shape))

    def _transpose(self, perm):
        return DenseEncoding(self._data.transpose(perm))

    @property
    def flat(self):
        return DenseEncoding(self._data.reshape((-1,)))

    def copy(self):
        return DenseEncoding(self._data.copy())


class SparseEncoding(Encoding):
    """
    `Encoding` implementation based on an ND sparse implementation.

    Since the scipy.sparse implementations are for 2D arrays only, this
    implementation uses a single-column CSC matrix with index
    raveling/unraveling.
    """

    def __init__(self, indices, values, shape=None):
        """
        Parameters
        ------------
        indices: (m, n)-sized int array of indices
        values: (m, n)-sized dtype array of values at the specified indices
        shape: (n,) iterable of integers. If None, the maximum value of indices
            + 1 is used.
        """
        data = caching.DataStore()
        super(SparseEncoding, self).__init__(data)
        data['indices'] = indices
        data['values'] = values
        indices = data['indices']
        if len(indices.shape) != 2:
            raise ValueError(
                'indices must be 2D, got shaped %s' % str(indices.shape))
        if data['values'].shape != (indices.shape[0],):
            raise ValueError(
                'values and indices shapes inconsistent: %s and %s'
                % (data['values'], data['indices']))
        if shape is None:
            self._shape = tuple(data['indices'].max(axis=0) + 1)
        else:
            self._shape = tuple(shape)
            if not np.all(indices < self._shape):
                raise ValueError('all indices must be less than shape')
        if not np.all(indices >= 0):
            raise ValueError('all indices must be non-negative')

    @staticmethod
    def from_dense(dense_data):
        sparse_indices = np.where(dense_data)
        values = dense_data[sparse_indices]
        return SparseEncoding(
            np.stack(sparse_indices, axis=-1), values, shape=dense_data.shape)

    def copy(self):
        return SparseEncoding(
            indices=self.sparse_indices.copy(),
            values=self.sparse_values.copy(),
            shape=self.shape)

    @property
    def sparse_indices(self):
        return self._data['indices']

    @property
    def sparse_values(self):
        return self._data['values']

    @property
    def dtype(self):
        return self.sparse_values.dtype

    @caching.cache_decorator
    def sum(self):
        return self.sparse_values.sum()

    @property
    def ndims(self):
        return self.sparse_indices.shape[-1]

    @property
    def shape(self):
        return self._shape

    @property
    def size(self):
        return np.prod(self.shape)

    @property
    def sparse_components(self):
        return self.sparse_indices, self.sparse_values

    @caching.cache_decorator
    def dense(self):
        sparse = self._csc
        # sparse.todense gives an `np.matrix` which cannot be reshaped
        dense = np.empty(shape=sparse.shape, dtype=sparse.dtype)
        sparse.todense(out=dense)
        return np.reshape(dense, self.shape)

    @caching.cache_decorator
    def _csc(self):
        values = self.sparse_values
        indices = self._flat_indices(self.sparse_indices)
        indptr = [0, len(indices)]
        return sp.csc_matrix((values, indices, indptr), shape=(self.size, 1))

    def _flat_indices(self, indices):
        assert indices.shape[1] == 3 and len(indices.shape) == 2
        return np.ravel_multi_index(indices.T, self.shape)

    def _shaped_indices(self, flat_indices):
        return np.column_stack(np.unravel_index(flat_indices, self.shape))

    def gather_nd(self, indices):
        mat = self._csc[self._flat_indices(indices)].todense()
        # mat is a np matrix, which stays rank 2 after squeeze
        # np.asarray changes this to a standard rank 2 array.
        return np.asarray(mat).squeeze(axis=-1)

    def mask(self, mask):
        i, _ = np.where(self._csc[mask.reshape((-1,))])
        return self._shaped_indices(i)

    def get_value(self, index):
        return self._gather_nd(np.expand_dims(index, axis=0))[0]

    @caching.cache_decorator
    def stripped(self):
        """
        Get encoding with all zeros stripped from the start/end of each axis.

        Returns:
            encoding: SparseEncoding with same values but indices shifted down
                by padding[:, 0]
            padding: (n, 2) array of ints denoting padding at the start/end
                that was stripped
        """
        if self.is_empty:
            return _empty_stripped(self.shape)
        indices = self.sparse_indices
        pad_left = np.min(indices, axis=0)
        pad_right = np.max(indices, axis=0)
        pad_right *= -1
        pad_right += self.shape
        padding = np.column_stack((pad_left, pad_right))
        return SparseEncoding(indices - pad_left, self.sparse_values), padding


def SparseBinaryEncoding(indices, shape=None):
    """
    Convenient factory constructor for SparseEncodings with values all ones.

    Parameters
    ------------
    indices: (m, n) sparse indices into conceptual rank-n array
    shape: length n iterable or None. If None, maximum of indices along first
        axis + 1 is used

    Returns
    ------------
    rank n bool `SparseEncoding` with True values at each index.
    """
    return SparseEncoding(
        indices, np.ones(shape=(indices.shape[0],), dtype=bool), shape)


class RunLengthEncoding(Encoding):
    """1D run length encoding.

    See `trimesh.voxel.runlength` documentation for implementation details.
    """

    def __init__(self, data, dtype=None):
        """
        Parameters
        ------------
        data: run length encoded data.
        dtype: dtype of encoded data. Each second value of data is cast will be
            cast to this dtype if provided.
        """
        super(RunLengthEncoding, self).__init__(
            data=caching.tracked_array(data))
        if dtype is None:
            dtype = self._data.dtype
        if len(self._data.shape) != 1:
            raise ValueError('data must be 1D numpy array')
        self._dtype = dtype

    @caching.cache_decorator
    def is_empty(self):
        return not np.any(
            np.logical_and(self._data[::2], self._data[1::2]))

    @property
    def ndims(self):
        return 1

    @property
    def shape(self):
        return (self.size,)

    @property
    def dtype(self):
        return self._dtype

    def crc(self):
        log.warning(
            '`geometry.crc()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def hash(self):
        log.warning(
            '`geometry.hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def __hash__(self):
        """
        Get the hash of the current transformation matrix.

        Returns
        ------------
        hash : str
          Hash of transformation matrix
        """
        return self._data.__hash__()

    @staticmethod
    def from_dense(dense_data, dtype=np.int64, encoding_dtype=np.int64):
        return RunLengthEncoding(
            runlength.dense_to_rle(dense_data, dtype=encoding_dtype), dtype=dtype)

    @staticmethod
    def from_rle(rle_data, dtype=None):
        if dtype != rle_data.dtype:
            rle_data = runlength.rle_to_rle(rle_data, dtype=dtype)
        return RunLengthEncoding(rle_data)

    @staticmethod
    def from_brle(brle_data, dtype=None):
        return RunLengthEncoding(runlength.brle_to_rle(brle_data, dtype=dtype))

    @caching.cache_decorator
    def stripped(self):
        if self.is_empty:
            return _empty_stripped(self.shape)
        data, padding = runlength.rle_strip(self._data)
        if padding == (0, 0):
            encoding = self
        else:
            encoding = RunLengthEncoding(data, dtype=self._dtype)
        padding = np.expand_dims(padding, axis=0)
        return encoding, padding

    @caching.cache_decorator
    def sum(self):
        return (self._data[::2] * self._data[1::2]).sum()

    @caching.cache_decorator
    def size(self):
        return runlength.rle_length(self._data)

    def _flip(self, axes):
        if axes != (0,):
            raise ValueError(
                'encoding is 1D - cannot flip on axis %s' % str(axes))
        return RunLengthEncoding(runlength.rle_reverse(self._data))

    @caching.cache_decorator
    def sparse_components(self):
        return runlength.rle_to_sparse(self._data)

    @caching.cache_decorator
    def sparse_indices(self):
        return self.sparse_components[0]

    @caching.cache_decorator
    def sparse_values(self):
        return self.sparse_components[1]

    @caching.cache_decorator
    def dense(self):
        return runlength.rle_to_dense(self._data, dtype=self._dtype)

    def gather(self, indices):
        return runlength.rle_gather_1d(self._data, indices, dtype=self._dtype)

    def gather_nd(self, indices):
        indices = np.squeeze(indices, axis=-1)
        return self.gather(indices)

    def sorted_gather(self, ordered_indices):
        return np.array(
            tuple(runlength.sorted_rle_gather_1d(self._data, ordered_indices)),
            dtype=self._dtype)

    def mask(self, mask):
        return np.array(
            tuple(runlength.rle_mask(self._data, mask)), dtype=self._dtype)

    def get_value(self, index):
        for value in self.sorted_gather((index,)):
            return np.asanyarray(value, dtype=self._dtype)

    def copy(self):
        return RunLengthEncoding(self._data.copy(), dtype=self.dtype)

    def run_length_data(self, dtype=np.int64):
        return runlength.rle_to_rle(self._data, dtype=dtype)

    def binary_run_length_data(self, dtype=np.int64):
        return runlength.rle_to_brle(self._data, dtype=dtype)


class BinaryRunLengthEncoding(RunLengthEncoding):
    """1D binary run length encoding.

    See `trimesh.voxel.runlength` documentation for implementation details.
    """

    def __init__(self, data):
        """
        Parameters
        ------------
        data: binary run length encoded data.
        """
        super(BinaryRunLengthEncoding, self).__init__(data=data, dtype=bool)

    @caching.cache_decorator
    def is_empty(self):
        return not np.any(self._data[1::2])

    @staticmethod
    def from_dense(dense_data, encoding_dtype=np.int64):
        return BinaryRunLengthEncoding(
            runlength.dense_to_brle(dense_data, dtype=encoding_dtype))

    @staticmethod
    def from_rle(rle_data, dtype=None):
        return BinaryRunLengthEncoding(
            runlength.rle_to_brle(rle_data, dtype=dtype))

    @staticmethod
    def from_brle(brle_data, dtype=None):
        if dtype != brle_data.dtype:
            brle_data = runlength.brle_to_brle(brle_data, dtype=dtype)
        return BinaryRunLengthEncoding(brle_data)

    @caching.cache_decorator
    def stripped(self):
        if self.is_empty:
            return _empty_stripped(self.shape)
        data, padding = runlength.rle_strip(self._data)
        if padding == (0, 0):
            encoding = self
        else:
            encoding = BinaryRunLengthEncoding(data)
        padding = np.expand_dims(padding, axis=0)
        return encoding, padding

    @caching.cache_decorator
    def sum(self):
        return self._data[1::2].sum()

    @caching.cache_decorator
    def size(self):
        return runlength.brle_length(self._data)

    def _flip(self, axes):
        if axes != (0,):
            raise ValueError(
                'encoding is 1D - cannot flip on axis %s' % str(axes))
        return BinaryRunLengthEncoding(runlength.brle_reverse(self._data))

    @property
    def sparse_components(self):
        return self.sparse_indices, self.sparse_values

    @caching.cache_decorator
    def sparse_values(self):
        return np.ones(shape=(self.sum,), dtype=bool)

    @caching.cache_decorator
    def sparse_indices(self):
        return runlength.brle_to_sparse(self._data)

    @caching.cache_decorator
    def dense(self):
        return runlength.brle_to_dense(self._data)

    def gather(self, indices):
        return runlength.brle_gather_1d(self._data, indices)

    def gather_nd(self, indices):
        indices = np.squeeze(indices)
        return self.gather(indices)

    def sorted_gather(self, ordered_indices):
        gen = runlength.sorted_brle_gather_1d(self._data, ordered_indices)
        return np.array(tuple(gen), dtype=bool)

    def mask(self, mask):
        gen = runlength.brle_mask(self._data, mask)
        return np.array(tuple(gen), dtype=bool)

    def copy(self):
        return BinaryRunLengthEncoding(self._data.copy())

    def run_length_data(self, dtype=np.int64):
        return runlength.brle_to_rle(self._data, dtype=dtype)

    def binary_run_length_data(self, dtype=np.int64):
        return runlength.brle_to_brle(self._data, dtype=dtype)


class LazyIndexMap(Encoding):
    """
    Abstract class for implementing lazy index mapping operations.

    Implementations include transpose, flatten/reshaping and flipping

    Derived classes must implement:
        * _to_base_indices(indices)
        * _from_base_indices(base_indices)
        * shape
        * dense
        * mask(mask)
    """

    @abc.abstractmethod
    def _to_base_indices(self, indices):
        pass

    @abc.abstractmethod
    def _from_base_indices(self, base_indices):
        pass

    @property
    def is_empty(self):
        return self._data.is_empty

    @property
    def dtype(self):
        return self._data.dtype

    @property
    def sum(self):
        return self._data.sum

    @property
    def size(self):
        return self._data.size

    @property
    def sparse_indices(self):
        return self._from_base_indices(self._data.sparse_indices)

    @property
    def sparse_values(self):
        return self._data.sparse_values

    def gather_nd(self, indices):
        return self._data.gather_nd(self._to_base_indices(indices))

    def get_value(self, index):
        return self._data[tuple(self._to_base_indices(index))]


class FlattenedEncoding(LazyIndexMap):
    """
    Lazily flattened encoding.

    Dense equivalent is np.reshape(data, (-1,)) (np.flatten creates a copy).
    """

    def _to_base_indices(self, indices):
        return np.column_stack(np.unravel_index(indices, self._data.shape))

    def _from_base_indices(self, base_indices):
        return np.expand_dims(
            np.ravel_multi_index(base_indices.T, self._data.shape), axis=-1)

    @property
    def shape(self):
        return self.size,

    @property
    def dense(self):
        return self._data.dense.reshape((-1,))

    def mask(self, mask):
        return self._data.mask(mask.reshape(self._data.shape))

    @property
    def flat(self):
        return self

    def copy(self):
        return FlattenedEncoding(self._data.copy())


class ShapedEncoding(LazyIndexMap):
    """
    Lazily reshaped encoding.

    Numpy equivalent is `np.reshape`
    """

    def __init__(self, encoding, shape):
        if isinstance(encoding, Encoding):
            if encoding.ndims != 1:
                encoding = encoding.flat
        else:
            raise ValueError('encoding must be an Encoding')
        super(ShapedEncoding, self).__init__(data=encoding)
        self._shape = tuple(shape)
        nn = self._shape.count(-1)
        size = np.prod(self._shape)
        if nn == 1:
            size = np.abs(size)
            if self._data.size % size != 0:
                raise ValueError(
                    'cannot reshape encoding of size %d into shape %s' %
                    (self._data.size, str(self._shape)))
            rem = self._data.size // size
            self._shape = tuple(rem if s == -1 else s for s in self._shape)
        elif nn > 2:
            raise ValueError('shape cannot have more than one -1 value')
        elif np.prod(self._shape) != self._data.size:
            raise ValueError(
                'cannot reshape encoding of size %d into shape %s' %
                (self._data.size, str(self._shape)))

    def _from_base_indices(self, base_indices):
        return np.column_stack(np.unravel_index(base_indices, self.shape))

    def _to_base_indices(self, indices):
        return np.expand_dims(
            np.ravel_multi_index(indices.T, self.shape), axis=-1)

    @property
    def flat(self):
        return self._data

    @property
    def shape(self):
        return self._shape

    @property
    def dense(self):
        return self._data.dense.reshape(self.shape)

    def mask(self, mask):
        return self._data.mask(mask.flat)

    def copy(self):
        return ShapedEncoding(encoding=self._data.copy(), shape=self.shape)


class TransposedEncoding(LazyIndexMap):
    """
    Lazily transposed encoding

    Dense equivalent is `np.transpose`
    """

    def __init__(self, base_encoding, perm):
        if not isinstance(base_encoding, Encoding):
            raise ValueError(
                'base_encoding must be an Encoding, got %s'
                % str(base_encoding))
        if len(base_encoding.shape) != len(perm):
            raise ValueError(
                'base_encoding has %d ndims - cannot transpose with perm %s'
                % (base_encoding.ndims, str(perm)))
        super(TransposedEncoding, self).__init__(base_encoding)
        perm = np.array(perm, dtype=np.int64)
        if not all(i in perm for i in range(base_encoding.ndims)):
            raise ValueError('perm %s is not a valid permutation' % str(perm))
        inv_perm = np.empty_like(perm)
        inv_perm[perm] = np.arange(base_encoding.ndims)
        self._perm = perm
        self._inv_perm = inv_perm

    def transpose(self, perm):
        return _transposed(self._data, [self._perm[p] for p in perm])

    def _transpose(self, perm):
        raise RuntimeError('Should not be here')

    @property
    def perm(self):
        return self._perm

    @property
    def shape(self):
        shape = self._data.shape
        return tuple(shape[p] for p in self._perm)

    def _to_base_indices(self, indices):
        return np.take(indices, self._perm, axis=-1)

    def _from_base_indices(self, base_indices):
        try:
            return np.take(base_indices, self._inv_perm, axis=-1)
        except TypeError:
            # windows sometimes tries to use wrong dtypes
            return np.take(base_indices.astype(np.int64),
                           self._inv_perm.astype(np.int64),
                           axis=-1)

    @property
    def dense(self):
        return self._data.dense.transpose(self._perm)

    def gather(self, indices):
        return self._data.gather(self._base_indices(indices))

    def mask(self, mask):
        return self._data.mask(
            mask.transpose(self._inv_perm)).transpose(self._perm)

    def get_value(self, index):
        return self._data[tuple(self._base_indices(index))]

    @property
    def data(self):
        return self._data

    def copy(self):
        return TransposedEncoding(
            base_encoding=self._data.copy(), perm=self._perm)


class FlippedEncoding(LazyIndexMap):
    """
    Encoding with entries flipped along one or more axes.

    Dense equivalent is `np.flip`
    """

    def __init__(self, encoding, axes):
        ndims = encoding.ndims
        if isinstance(axes, np.ndarray) and axes.size == 1:
            axes = axes.item(),
        elif isinstance(axes, int):
            axes = axes,
        axes = tuple(a + ndims if a < 0 else a for a in axes)
        self._axes = tuple(sorted(axes))
        if len(set(self._axes)) != len(self._axes):
            raise ValueError(
                "Axes cannot contain duplicates, got %s" % str(self._axes))
        super(FlippedEncoding, self).__init__(encoding)
        if not all(0 <= a < self._data.ndims for a in axes):
            raise ValueError(
                'Invalid axes %s for %d-d encoding'
                % (str(axes), self._data.ndims))

    def _to_base_indices(self, indices):
        indices = indices.copy()
        shape = self.shape
        for a in self._axes:
            indices[:, a] *= -1
            indices[:, a] += shape
        return indices

    def _from_base_indices(self, base_indices):
        return self._to_base_indices(base_indices)

    @property
    def shape(self):
        return self._data.shape

    @property
    def dense(self):
        dense = self._data.dense
        for a in self._axes:
            dense = np.flip(dense, a)
        return dense

    def mask(self, mask):
        if not isinstance(mask, Encoding):
            mask = DenseEncoding(mask)
        mask = mask.flip(self._axes)
        return self._data.mask(mask).flip(self._axes)

    def copy(self):
        return FlippedEncoding(self._data.copy(), self._axes)

    def flip(self, axis=0):
        if isinstance(axis, np.ndarray):
            if axis.size == 1:
                axis = axis.item(),
            else:
                axis = tuple(axis)
        elif isinstance(axis, int):
            axes = axis,
        else:
            axes = tuple(axis)
        return _flipped(self, self._axes + axes)

    def _flip(self, axes):
        raise RuntimeError('Should not be here')


def _flipped(encoding, axes):
    if not hasattr(axes, '__iter__'):
        axes = axes,
    unique_ax = set()
    ndims = encoding.ndims
    axes = tuple(a + ndims if a < 0 else a for a in axes)
    for a in axes:
        if a in unique_ax:
            unique_ax.remove(a)
        else:
            unique_ax.add(a)
    if len(unique_ax) == 0:
        return encoding
    else:
        return encoding._flip(tuple(sorted(unique_ax)))


def _transposed(encoding, perm):
    ndims = encoding.ndims
    perm = tuple(p + ndims if p < 0 else p for p in perm)
    if np.all(np.arange(ndims) == perm):
        return encoding
    else:
        return encoding._transpose(perm)


# trimesh/voxel/morphology.py

"""Basic morphology operations that create new encodings."""
import numpy as np

from . import encoding as enc
from . import ops

from ..constants import log_time
from .. import util


try:
    from scipy import ndimage
except BaseException as E:
    # scipy is a soft dependency
    from ..exceptions import ExceptionWrapper
    ndimage = ExceptionWrapper(E)


def _dense(encoding, rank=None):
    if isinstance(encoding, np.ndarray):
        dense = encoding
    elif isinstance(encoding, enc.Encoding):
        dense = encoding.dense
    else:
        raise ValueError(
            'encoding must be np.ndarray or Encoding, got %s' % str(encoding))
    if rank:
        _assert_rank(dense, rank)
    return dense


def _sparse_indices(encoding, rank=None):
    if isinstance(encoding, np.ndarray):
        sparse_indices = encoding
    elif isinstance(encoding, enc.Encoding):
        sparse_indices = encoding.sparse_indices
    else:
        raise ValueError(
            'encoding must be np.ndarray or Encoding, got %s' % str(encoding))

    _assert_sparse_rank(sparse_indices, 3)
    return sparse_indices


def _assert_rank(value, rank):
    if len(value.shape) != rank:
        raise ValueError(
            'Expected rank %d, got shape %s' % (rank, str(value.shape)))


def _assert_sparse_rank(value, rank=None):
    if len(value.shape) != 2:
        raise ValueError(
            'sparse_indices must be rank 2, got shape %s' % str(value.shape))
    if rank is not None:
        if value.shape[-1] != rank:
            raise ValueError(
                'sparse_indices.shape[1] must be %d, got %d'
                % (rank, value.shape[-1]))


@log_time
def fill_base(encoding):
    """
    Given a sparse surface voxelization, fill in between columns.

    Parameters
    --------------
    encoding: Encoding object or sparse array with shape (?, 3)

    Returns
    --------------
    A new filled encoding object.
    """
    return enc.SparseBinaryEncoding(
        ops.fill_base(_sparse_indices(encoding, rank=3)))


@log_time
def fill_orthographic(encoding):
    """
    Fill the given encoding by orthographic projection method.

    Any voxel in the dense representation with no free ray along the x, y, z
    axes in each direction is assigned filled. This is likely faster than fill
    holes, and is more stable with regards to small holes.

    Parameters
    --------------
    encoding: Encoding object or dense rank-3 array.

    Returns
    --------------
    A new filled encoding object.
    """
    return enc.DenseEncoding(ops.fill_orthographic(_dense(encoding, rank=3)))


@log_time
def fill_holes(encoding, **kwargs):
    """
    Encoding wrapper around scipy.ndimage.morphology.binary_fill_holes.

    https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.ndimage.morphology.binary_fill_holes.html#scipy.ndimage.morphology.binary_fill_holes

    Parameters
    --------------
    encoding: Encoding object or dense rank-3 array.
    **kwargs: see scipy.ndimage.morphology.binary_fill_holes.

    Returns
    --------------
    A new filled in encoding object.
    """
    return enc.DenseEncoding(
        ndimage.binary_fill_holes(_dense(encoding, rank=3), **kwargs))


fillers = util.FunctionRegistry(
    base=fill_base,
    orthographic=fill_orthographic,
    holes=fill_holes,
)


def fill(encoding, method='base', **kwargs):
    """
    Fill the given encoding using the specified implementation.

    See `fillers` for available implementations or to add your own, e.g. via
    `fillers['custom_key'] = custom_fn`.

    `custom_fn` should have signature `(encoding, **kwargs) -> filled_encoding`
    and should not modify encoding.

    Parameters
    --------------
    encoding: Encoding object (left unchanged).
    method: method present in `fillers`.
    **kwargs: additional kwargs passed to the specified implementation.

    Returns
    --------------
    A new filled Encoding object.
    """
    return fillers(method, encoding=encoding, **kwargs)


def binary_dilation(encoding, **kwargs):
    """
    Encoding wrapper around scipy.ndimage.morphology.binary_dilation.

    https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.ndimage.morphology.binary_dilation.html#scipy.ndimage.morphology.binary_dilation
    """
    return enc.DenseEncoding(
        ndimage.binary_dilation(_dense(encoding, rank=3), **kwargs))


def binary_closing(encoding, **kwargs):
    """
    Encoding wrapper around scipy.ndimage.morphology.binary_closing.

    https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.ndimage.morphology.binary_closing.html#scipy.ndimage.morphology.binary_closing
    """
    return enc.DenseEncoding(
        ndimage.binary_closing(_dense(encoding, rank=3), **kwargs))


def surface(encoding, structure=None):
    """
    Get elements on the surface of encoding.

    A surface element is any one in encoding that is adjacent to an empty
    voxel.

    Parameters
    --------------
    encoding: Encoding or dense rank-3 array
    structure: adjacency structure. If None, square connectivity is used.

    Returns
    --------------
    new surface Encoding.
    """
    dense = _dense(encoding, rank=3)
    # padding/unpadding resolves issues with occupied voxels on the boundary
    dense = np.pad(dense, np.ones((3, 2), dtype=int), mode='constant')
    empty = np.logical_not(dense)
    dilated = ndimage.binary_dilation(empty, structure=structure)
    surface = np.logical_and(dense, dilated)[1:-1, 1:-1, 1:-1]
    return enc.DenseEncoding(surface)


# trimesh/voxel/ops.py

import numpy as np

from .. import util
from ..constants import log


def fill_orthographic(dense):
    shape = dense.shape
    indices = np.stack(
        np.meshgrid(*(np.arange(s) for s in shape), indexing='ij'),
        axis=-1)
    empty = np.logical_not(dense)

    def fill_axis(axis):
        base_local_indices = indices[..., axis]
        local_indices = base_local_indices.copy()
        local_indices[empty] = shape[axis]
        mins = np.min(local_indices, axis=axis, keepdims=True)
        local_indices = base_local_indices.copy()
        local_indices[empty] = -1
        maxs = np.max(local_indices, axis=axis, keepdims=True)

        return np.logical_and(
            base_local_indices >= mins,
            base_local_indices <= maxs,
        )

    filled = fill_axis(axis=0)
    for axis in range(1, len(shape)):
        filled = np.logical_and(filled, fill_axis(axis))
    return filled


def fill_base(sparse_indices):
    """
    Given a sparse surface voxelization, fill in between columns.

    Parameters
    --------------
    sparse_indices: (n, 3) int, location of filled cells

    Returns
    --------------
    filled: (m, 3) int, location of filled cells
    """
    # validate inputs
    sparse_indices = np.asanyarray(sparse_indices, dtype=np.int64)
    if not util.is_shape(sparse_indices, (-1, 3)):
        raise ValueError('incorrect shape')

    # create grid and mark inner voxels
    max_value = sparse_indices.max() + 3

    grid = np.zeros((max_value,
                     max_value,
                     max_value),
                    bool)
    voxels_sparse = np.add(sparse_indices, 1)

    grid[tuple(voxels_sparse.T)] = 1

    for i in range(max_value):
        check_dir2 = False
        for j in range(0, max_value - 1):
            idx = []
            # find transitions first
            # transition positions are from 0 to 1 and from 1 to 0
            eq = np.equal(grid[i, j, :-1], grid[i, j, 1:])
            idx = np.where(np.logical_not(eq))[0] + 1
            c = len(idx)
            check_dir2 = (c % 4) > 0 and c > 4
            if c < 4:
                continue
            for s in range(0, c - c % 4, 4):
                grid[i, j, idx[s]:idx[s + 3]] = 1
        if not check_dir2:
            continue

        # check another direction for robustness
        for k in range(0, max_value - 1):
            idx = []
            # find transitions first
            eq = np.equal(grid[i, :-1, k], grid[i, 1:, k])
            idx = np.where(np.logical_not(eq))[0] + 1
            c = len(idx)
            if c < 4:
                continue
            for s in range(0, c - c % 4, 4):
                grid[i, idx[s]:idx[s + 3], k] = 1

    # generate new voxels
    filled = np.column_stack(np.where(grid))
    filled -= 1

    return filled


fill_voxelization = fill_base


def matrix_to_marching_cubes(matrix, pitch=1.0):
    """
    Convert an (n, m, p) matrix into a mesh, using marching_cubes.

    Parameters
    -----------
    matrix : (n, m, p) bool
      Occupancy array

    Returns
    ----------
    mesh : trimesh.Trimesh
      Mesh generated by meshing voxels using
      the marching cubes algorithm in skimage
    """
    from skimage import measure
    from ..base import Trimesh

    matrix = np.asanyarray(matrix, dtype=bool)

    rev_matrix = np.logical_not(matrix)  # Takes set about 0.
    # Add in padding so marching cubes can function properly with
    # voxels on edge of AABB
    pad_width = 1
    rev_matrix = np.pad(rev_matrix,
                        pad_width=(pad_width),
                        mode='constant',
                        constant_values=(1))

    # pick between old and new API
    if hasattr(measure, 'marching_cubes_lewiner'):
        func = measure.marching_cubes_lewiner
    else:
        func = measure.marching_cubes

    # Run marching cubes.
    pitch = np.asanyarray(pitch)
    if pitch.size == 1:
        pitch = (pitch,) * 3
    meshed = func(volume=rev_matrix,
                  level=.5,  # it is a boolean voxel grid
                  spacing=pitch)

    # allow results from either marching cubes function in skimage
    # binaries available for python 3.3 and 3.4 appear to use the classic
    # method
    if len(meshed) == 2:
        log.warning('using old marching cubes, may not be watertight!')
        vertices, faces = meshed
        normals = None
    elif len(meshed) == 4:
        vertices, faces, normals, vals = meshed

    # Return to the origin, add in the pad_width
    vertices = np.subtract(vertices, pad_width * pitch)
    # create the mesh
    mesh = Trimesh(vertices=vertices,
                   faces=faces,
                   vertex_normals=normals)
    return mesh


def sparse_to_matrix(sparse):
    """
    Take a sparse (n,3) list of integer indexes of filled cells,
    turn it into a dense (m,o,p) matrix.

    Parameters
    -----------
    sparse : (n, 3) int
      Index of filled cells

    Returns
    ------------
    dense : (m, o, p) bool
      Matrix of filled cells
    """

    sparse = np.asanyarray(sparse, dtype=np.int64)
    if not util.is_shape(sparse, (-1, 3)):
        raise ValueError('sparse must be (n,3)!')

    shape = sparse.max(axis=0) + 1
    matrix = np.zeros(np.prod(shape), dtype=bool)
    multiplier = np.array([np.prod(shape[1:]), shape[2], 1])

    index = (sparse * multiplier).sum(axis=1)
    matrix[index] = True

    dense = matrix.reshape(shape)
    return dense


def points_to_marching_cubes(points, pitch=1.0):
    """
    Mesh points by assuming they fill a voxel box, and then
    running marching cubes on them

    Parameters
    ------------
    points : (n, 3) float
      Points in 3D space

    Returns
    -------------
    mesh : trimesh.Trimesh
      Points meshed using marching cubes
    """
    # make sure inputs are as expected
    points = np.asanyarray(points, dtype=np.float64)
    pitch = np.asanyarray(pitch, dtype=float)

    # find the minimum value of points for origin
    origin = points.min(axis=0)
    # convert points to occupied voxel cells
    index = ((points - origin) / pitch).round().astype(np.int64)

    # convert voxel indices to a matrix
    matrix = sparse_to_matrix(index)

    # run marching cubes on the matrix to generate a mesh
    mesh = matrix_to_marching_cubes(matrix, pitch=pitch)
    mesh.vertices += origin

    return mesh


def multibox(centers, pitch=1.0, colors=None):
    """
    Return a Trimesh object with a box at every center.

    Doesn't do anything nice or fancy.

    Parameters
    -----------
    centers : (n, 3) float
      Center of boxes that are occupied
    pitch : float
      The edge length of a voxel
    colors : (3,) or (4,) or (n,3) or (n, 4) float
      Color of boxes

    Returns
    ---------
    rough : Trimesh
      Mesh object representing inputs
    """
    from .. import primitives
    from ..base import Trimesh

    # get centers as numpy array
    centers = np.asanyarray(
        centers, dtype=np.float64)

    # get a basic box
    b = primitives.Box()
    # apply the pitch
    b.apply_scale(float(pitch))
    # tile into one box vertex per center
    v = np.tile(
        centers,
        (1, len(b.vertices))).reshape((-1, 3))
    # offset to centers
    v += np.tile(b.vertices, (len(centers), 1))

    f = np.tile(b.faces, (len(centers), 1))
    f += np.tile(
        np.arange(len(centers)) * len(b.vertices),
        (len(b.faces), 1)).T.reshape((-1, 1))

    face_colors = None
    if colors is not None:
        colors = np.asarray(colors)
        if colors.ndim == 1:
            colors = colors[None].repeat(len(centers), axis=0)
        if colors.ndim == 2 and len(colors) == len(centers):
            face_colors = colors.repeat(12, axis=0)

    mesh = Trimesh(vertices=v,
                   faces=f,
                   face_colors=face_colors)

    return mesh


def boolean_sparse(a, b, operation=np.logical_and):
    """
    Find common rows between two arrays very quickly
    using 3D boolean sparse matrices.

    Parameters
    -----------
    a: (n, d)  int, coordinates in space
    b: (m, d)  int, coordinates in space
    operation: numpy operation function, ie:
                  np.logical_and
                  np.logical_or

    Returns
    -----------
    coords: (q, d) int, coordinates in space
    """
    # 3D sparse arrays, using wrapped scipy.sparse
    # pip install sparse
    import sparse

    # find the bounding box of both arrays
    extrema = np.array([a.min(axis=0),
                        a.max(axis=0),
                        b.min(axis=0),
                        b.max(axis=0)])
    origin = extrema.min(axis=0) - 1
    size = tuple(extrema.ptp(axis=0) + 2)

    # put nearby voxel arrays into same shape sparse array
    sp_a = sparse.COO((a - origin).T,
                      data=np.ones(len(a), dtype=bool),
                      shape=size)
    sp_b = sparse.COO((b - origin).T,
                      data=np.ones(len(b), dtype=bool),
                      shape=size)

    # apply the logical operation
    # get a sparse matrix out
    applied = operation(sp_a, sp_b)
    # reconstruct the original coordinates
    coords = np.column_stack(applied.coords) + origin

    return coords


def strip_array(data):
    shape = data.shape
    ndims = len(shape)
    padding = []
    slices = []
    for dim in range(len(shape)):
        axis = tuple(range(dim)) + tuple(range(dim + 1, ndims))
        filled = np.any(data, axis=axis)
        indices, = np.nonzero(filled)
        pad_left = indices[0]
        pad_right = indices[-1]
        padding.append([pad_left, pad_right])
        slices.append(slice(pad_left, pad_right))
    return data[tuple(slices)], np.array(padding, int)


def indices_to_points(indices, pitch=None, origin=None):
    """
    Convert indices of an (n,m,p) matrix into a set of voxel center points.

    Parameters
    ----------
    indices: (q, 3) int, index of voxel matrix (n,m,p)
    pitch: float, what pitch was the voxel matrix computed with
    origin: (3,) float, what is the origin of the voxel matrix

    Returns
    ----------
    points: (q, 3) float, list of points
    """
    indices = np.asanyarray(indices)
    if indices.shape[1:] != (3,):
        raise ValueError('shape of indices must be (q, 3)')

    points = np.array(indices, dtype=np.float64)
    if pitch is not None:
        points *= float(pitch)
    if origin is not None:
        origin = np.asanyarray(origin)
        if origin.shape != (3,):
            raise ValueError('shape of origin must be (3,)')
        points += origin

    return points


def matrix_to_points(matrix, pitch=None, origin=None):
    """
    Convert an (n,m,p) matrix into a set of points for each voxel center.

    Parameters
    -----------
    matrix: (n,m,p) bool, voxel matrix
    pitch: float, what pitch was the voxel matrix computed with
    origin: (3,) float, what is the origin of the voxel matrix

    Returns
    ----------
    points: (q, 3) list of points
    """
    indices = np.column_stack(np.nonzero(matrix))
    points = indices_to_points(indices=indices,
                               pitch=pitch,
                               origin=origin)
    return points


def points_to_indices(points, pitch=None, origin=None):
    """
    Convert center points of an (n,m,p) matrix into its indices.

    Parameters
    ----------
    points : (q, 3) float
      Center points of voxel matrix (n,m,p)
    pitch : float
      What pitch was the voxel matrix computed with
    origin : (3,) float
      What is the origin of the voxel matrix

    Returns
    ----------
    indices : (q, 3) int
      List of indices
    """
    points = np.array(points, dtype=np.float64)
    if points.shape != (points.shape[0], 3):
        raise ValueError('shape of points must be (q, 3)')

    if origin is not None:
        origin = np.asanyarray(origin)
        if origin.shape != (3,):
            raise ValueError('shape of origin must be (3,)')
        points -= origin
    if pitch is not None:
        points /= pitch

    origin = np.asanyarray(origin, dtype=np.float64)
    pitch = float(pitch)

    indices = np.round(points).astype(int)
    return indices


# trimesh/voxel/runlength.py

"""
Numpy encode/decode/utility implementations for run length encodings.

# Run Length Encoded Features

Encoding/decoding functions for run length encoded data.

We include code for two variations:

* run length encoding (RLE)
* binary run length encdoing (BRLE)

RLE stores sequences of repeated values as the value followed by its count, e.g.

```python
dense_to_rle([5, 5, 3, 2, 2, 2, 2, 6]) == [5, 2, 3, 1, 2, 4, 6, 1]
```

i.e. the value `5` is repeated `2` times, then `3` is repeated `1` time, `2` is
repeated `4` times and `6` is repeated `1` time.

BRLE is an optimized form for when the stored values can only be `0` or `1`.
This means we only need to save the counts, and assume the values alternate
(starting at `0`).

```python
dense_to_brle([1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0]) == \
              [0, 2, 4, 7, 2]
```

i.e. the value zero occurs `0` times, followed by `2` ones, `4` zeros, `7` ones
and `2` zeros.

Sequences with counts exceeding the data type's maximum value have to be
handled carefully. For example, the `uint8` encoding of 300 zeros
(`uint8` has a max value of 255) is:

* RLE: `[0, 255, 0, 45]`  (`0` repeated `255` times + `0` repeated `45` times)
* BRLE: `[255, 0, 45, 0]` (`255` zeros + `0` ones + `45` zeros + `0` ones)

This module contains implementations of various RLE/BRLE operations.
"""
import functools
import numpy as np


def brle_length(brle):
    """Optimized implementation of `len(brle_to_dense(brle))`"""
    return np.sum(brle)


def rle_length(rle):
    """Optimized implementation of `len(rle_to_dense(rle_to_brle(rle)))`"""
    return np.sum(rle[1::2])


def rle_to_brle(rle, dtype=None):
    """
    Convert run length encoded (RLE) value/counts to BRLE.

    RLE data is stored in a rank 1 array with each pair giving:
    (value, count)

    e.g. the RLE encoding of [4, 4, 4, 1, 1, 6] is [4, 3, 1, 2, 6, 1].

    Parameters
    ----------
    rle : (n,) int
      Run length encoded data

    Returns
    ----------
    equivalent binary run length encoding. a list if dtype is None,
        otherwise brle_to_brle is called on that list before returning.

    Raises
    ----------
    ValueError
      If any of the even counts of `rle` are not zero or 1.
    """
    curr_val = 0
    out = [0]
    acc = 0
    for value, count in np.reshape(rle, (-1, 2)):
        acc += count
        if value not in (0, 1):
            raise ValueError(
                "Invalid run length encoding for conversion to BRLE")
        if value == curr_val:
            out[-1] += count
        else:
            out.append(int(count))
            curr_val = value
    if len(out) % 2:
        out.append(0)
    if dtype is not None:
        out = brle_to_brle(out, dtype=dtype)
    return out


def brle_logical_not(brle):
    """
    Get the BRLE encoding of the `logical_not`ed dense form of `brle`.

    Equivalent to `dense_to_brle(np.logical_not(brle_to_dense(brle)))` but
    highly optimized - just pads brle with a 0 on each end (or strips is
    existing endpoints are both zero).

    Parameters
    ----------
    brle: rank 1 int array of binary run length encoded data

    Returns
    ----------
    rank 1 int array of binary run length encoded data corresponding to
    element-wise not of the input.
    """
    if brle[0] or brle[-1]:
        return np.pad(brle, [1, 1], mode='constant')
    else:
        return brle[1:-1]


def merge_brle_lengths(lengths):
    """Inverse of split_long_brle_lengths."""
    if len(lengths) == 0:
        return []

    out = [int(lengths[0])]
    accumulating = False
    for length in lengths[1:]:
        if accumulating:
            out[-1] += length
            accumulating = False
        else:
            if length == 0:
                accumulating = True
            else:
                out.append(int(length))
    return out


def split_long_brle_lengths(lengths, dtype=np.int64):
    """
    Split lengths that exceed max dtype value.

    Lengths `l` are converted into [max_val, 0] * l // max_val + [l % max_val]

    e.g. for dtype=np.uint8 (max_value == 255)
    ```
    split_long_brle_lengths([600, 300, 2, 6], np.uint8) == \
             [255, 0, 255, 0, 90, 255, 0, 45, 2, 6]
    ```
    """
    lengths = np.asarray(lengths)
    max_val = np.iinfo(dtype).max
    bad_length_mask = lengths > max_val
    if np.any(bad_length_mask):
        # there are some bad lengths
        nl = len(lengths)
        repeats = np.asarray(lengths) // max_val
        remainders = (lengths % max_val).astype(dtype)

        lengths = np.concatenate(
            [np.array([max_val, 0] * repeat + [remainder], dtype=dtype)
             for repeat, remainder in zip(repeats, remainders)])
        lengths = lengths.reshape((np.sum(repeats) * 2 + nl,)).astype(dtype)
        return lengths
    elif lengths.dtype != dtype:
        return lengths.astype(dtype)
    else:
        return lengths


def dense_to_brle(dense_data, dtype=np.int64):
    """
    Get the binary run length encoding of `dense_data`.

    Parameters
    ----------
    dense_data: rank 1 bool array of data to encode.
    dtype: numpy int type.

    Returns
    ----------
    Binary run length encoded rank 1 array of dtype `dtype`.

    Raises
    ----------
    ValuError if dense_data is not a rank 1 bool array.
    """
    if dense_data.dtype != bool:
        raise ValueError("`dense_data` must be bool")
    if len(dense_data.shape) != 1:
        raise ValueError("`dense_data` must be rank 1.")
    n = len(dense_data)
    starts = np.r_[0, np.flatnonzero(dense_data[1:] != dense_data[:-1]) + 1]
    lengths = np.diff(np.r_[starts, n])
    lengths = split_long_brle_lengths(lengths, dtype=dtype)
    if dense_data[0]:
        lengths = np.pad(lengths, [1, 0], mode='constant')
    return lengths


_ft = np.array([False, True], dtype=bool)


def brle_to_dense(brle_data, vals=None):
    """Decode binary run length encoded data to dense.

    Parameters
    ----------
    brle_data: BRLE counts of False/True values
    vals: if not `None`, a length 2 array/list/tuple with False/True substitute
        values, e.g. brle_to_dense([2, 3, 1, 0], [7, 9]) == [7, 7, 9, 9, 9, 7]

    Returns
    ----------
    rank 1 dense data of dtype `bool if vals is None else vals.dtype`

    Raises
    ----------
    ValueError if vals it not None and shape is not (2,)
    """
    if vals is None:
        vals = _ft
    else:
        vals = np.asarray(vals)
        if vals.shape != (2,):
            raise ValueError("vals.shape must be (2,), got %s" % (vals.shape))
    ft = np.repeat(
        _ft[np.newaxis, :], (len(brle_data) + 1) // 2, axis=0).flatten()
    return np.repeat(ft[:len(brle_data)], brle_data).flatten()


def rle_to_dense(rle_data, dtype=np.int64):
    """Get the dense decoding of the associated run length encoded data."""
    values, counts = np.split(np.reshape(rle_data, (-1, 2)), 2, axis=-1)
    if dtype is not None:
        values = np.asanyarray(values, dtype=dtype)
    try:
        result = np.repeat(np.squeeze(values, axis=-1),
                           np.squeeze(counts, axis=-1))
    except TypeError:
        # on windows it sometimes fails to cast data type
        result = np.repeat(np.squeeze(values.astype(np.int64), axis=-1),
                           np.squeeze(counts.astype(np.int64), axis=-1))
    return result


def dense_to_rle(dense_data, dtype=np.int64):
    """Get run length encoding of the provided dense data."""
    n = len(dense_data)
    starts = np.r_[0, np.flatnonzero(dense_data[1:] != dense_data[:-1]) + 1]
    lengths = np.diff(np.r_[starts, n])
    values = dense_data[starts]
    values, lengths = split_long_rle_lengths(values, lengths, dtype=dtype)
    out = np.stack((values, lengths), axis=1)
    return out.flatten()


def split_long_rle_lengths(values, lengths, dtype=np.int64):
    """
    Split long lengths in the associated run length encoding.

    e.g.
    ```python
    split_long_rle_lengths([5, 300, 2, 12], np.uint8) == [5, 255, 5, 45, 2, 12]
    ```

    Parameters
    ----------
    values: values column of run length encoding, or `rle[::2]`
    lengths: counts in run length encoding, or `rle[1::2]`
    dtype: numpy data type indicating the maximum value.

    Returns
    ----------
    values, lengths associated with the appropriate splits. `lengths` will be
    of type `dtype`, while `values` will be the same as the value passed in.
    """
    max_length = np.iinfo(dtype).max
    lengths = np.asarray(lengths)
    repeats = lengths // max_length
    if np.any(repeats):
        repeats += 1
        remainder = lengths % max_length
        values = np.repeat(values, repeats)
        lengths = np.empty(len(repeats), dtype=dtype)
        lengths.fill(max_length)
        lengths = np.repeat(lengths, repeats)
        lengths[np.cumsum(repeats) - 1] = remainder
    elif lengths.dtype != dtype:
        lengths = lengths.astype(dtype)
    return values, lengths


def merge_rle_lengths(values, lengths):
    """Inverse of split_long_rle_lengths except returns normal python lists."""
    ret_values = []
    ret_lengths = []
    curr = None
    for value, length in zip(values, lengths):
        if length == 0:
            continue
        if value == curr:
            ret_lengths[-1] += length
        else:
            curr = value
            ret_lengths.append(int(length))
            ret_values.append(value)
    return ret_values, ret_lengths


def brle_to_rle(brle, dtype=np.int64):
    if len(brle) % 2 == 1:
        brle = np.concatenate([brle, [0]])
    lengths = brle
    values = np.tile(_ft, len(brle) // 2)
    return rle_to_rle(
        np.stack((values, lengths), axis=1).flatten(), dtype=dtype)


def brle_to_brle(brle, dtype=np.int64):
    """
    Almost the identity function.

    Checks for possible merges and required splits.
    """
    return split_long_brle_lengths(merge_brle_lengths(brle), dtype=dtype)


def rle_to_rle(rle, dtype=np.int64):
    """
    Almost the identity function.

    Checks for possible merges and required splits.
    """
    values, lengths = np.reshape(rle, (-1, 2)).T
    values, lengths = merge_rle_lengths(values, lengths)
    values, lengths = split_long_rle_lengths(values, lengths, dtype=dtype)
    return np.stack((values, lengths), axis=1).flatten()


def _unsorted_gatherer(indices, sorted_gather_fn):
    if not isinstance(indices, np.ndarray):
        indices = np.array(indices, copy=False)
    order = np.argsort(indices)
    ordered_indices = indices[order]

    def f(data, dtype=None):
        result = np.empty(
            len(order), dtype=dtype or getattr(
                data, 'dtype', None))
        result[order] = tuple(sorted_gather_fn(data, ordered_indices))
        return result

    return f


def sorted_rle_gather_1d(rle_data, ordered_indices):
    """
    Gather brle_data at ordered_indices.

    This is equivalent to `rle_to_dense(brle_data)[ordered_indices]` but avoids
    the decoding.

    Parameters
    ----------
    brle_data: iterable of run-length-encoded data.
    ordered_indices: iterable of ints in ascending order.

    Returns
    ----------
    `brle_data` iterable of values at the dense indices, same length as
    ordered indices.
    """
    data_iter = iter(rle_data)
    index_iter = iter(ordered_indices)
    try:
        index = next(index_iter)
    except StopIteration:
        return
    start = 0
    while True:
        while start <= index:
            try:
                value = next(data_iter)
                start += next(data_iter)
            except StopIteration:
                raise IndexError(
                    'Index %d out of range of raw_values length %d'
                    % (index, start))
        try:
            while index < start:
                yield value
                index = next(index_iter)
        except StopIteration:
            break


def rle_mask(rle_data, mask):
    """
    Perform masking of the input run-length data.

    Parameters
    ----------
    rle_data: iterable of run length encoded data
    mask: iterable of bools corresponding to the dense mask.

    Returns
    ----------
    iterable of dense values of rle_data wherever mask is True.
    """
    data_iter = iter(rle_data)
    mask_iter = iter(mask)
    while True:
        try:
            value = next(data_iter)
            count = next(data_iter)
        except StopIteration:
            break
        for _ in range(count):
            m = next(mask_iter)
            if m:
                yield value


def brle_mask(rle_data, mask):
    """
    Perform masking of the input binary run-length data.

    Parameters
    ----------
    brle_data: iterable of binary run length encoded data
    mask: iterable of bools corresponding to the dense mask.

    Returns
    ----------
    iterable dense values of brle_data wherever mask is True.
    """
    data_iter = iter(rle_data)
    mask_iter = iter(mask)
    value = True
    while True:
        try:
            value = not value
            count = next(data_iter)
        except StopIteration:
            break
        for _ in range(count):
            m = next(mask_iter)
            if m:
                yield value


def rle_gatherer_1d(indices):
    """
    Get a gather function at the given indices.

    Because gathering on RLE data requires sorting, for instances where
    gathering at the same indices on different RLE data this can save the
    sorting process.

    If only gathering on a single RLE iterable, use `rle_gather_1d`.

    Parameters
    ----------
    indices: iterable of integers

    Returns
    ----------
    gather function, mapping `(rle_data, dtype=None) -> values`.
    `values` will have the same length as `indices` and dtype provided,
    or rle_data.dtype if no dtype is provided.
    """
    return _unsorted_gatherer(indices, sorted_rle_gather_1d)


def rle_gather_1d(rle_data, indices, dtype=None):
    """
    Gather RLE data values at the provided dense indices.

    This is equivalent to `rle_to_dense(rle_data)[indices]` but the
    implementation does not require the construction of the dense array.

    If indices is known to be in order, use `sorted_gather_1d`.

    Parameters
    ----------
    rle_data: run length encoded data
    indices: dense indices
    dtype: numpy dtype. If not provided, uses rle_data.dtype

    Returns
    ----------
    numpy array, dense data at indices, same length as indices and dtype as
    rle_data
    """
    return rle_gatherer_1d(indices)(rle_data, dtype=dtype)


def sorted_brle_gather_1d(brle_data, ordered_indices):
    """
    Gather brle_data at ordered_indices.

    This is equivalent to `brle_to_dense(brle_data)[ordered_indices]` but
    avoids the decoding.

    Parameters
    ----------
    raw_data: iterable of run-length-encoded data.
    ordered_indices: iterable of ints in ascending order.

    Returns
    ----------
    `raw_data` iterable of values at the dense indices, same length as
    ordered indices.
    """
    data_iter = iter(brle_data)
    index_iter = iter(ordered_indices)
    try:
        index = next(index_iter)
    except StopIteration:
        return
    start = 0
    value = True
    while True:
        while start <= index:
            try:
                value = not value
                start += next(data_iter)
            except StopIteration:
                raise IndexError(
                    'Index %d out of range of raw_values length %d'
                    % (index, start))
        try:
            while index < start:
                yield value
                index = next(index_iter)
        except StopIteration:
            break


def brle_gatherer_1d(indices):
    """
    Get a gather function at the given indices.

    Because gathering on BRLE data requires sorting, for instances where
    gathering at the same indices on different RLE data this can save the
    sorting process.

    If only gathering on a single RLE iterable, use `brle_gather_1d`.

    Parameters
    ----------
    indices: iterable of integers

    Returns
    ----------
    gather function, mapping `(rle_data, dtype=None) -> values`.
    `values` will have the same length as `indices` and dtype provided,
    or rle_data.dtype if no dtype is provided.
    """
    return functools.partial(
        _unsorted_gatherer(indices, sorted_brle_gather_1d), dtype=bool)


def brle_gather_1d(brle_data, indices):
    """
    Gather BRLE data values at the provided dense indices.

    This is equivalent to `rle_to_dense(rle_data)[indices]` but the
    implementation does not require the construction of the dense array.

    If indices is known to be in order, use `sorted_brle_gather_1d`.

    Parameters
    ----------
    rle_data: run length encoded data
    indices: dense indices

    Returns
    ----------
    numpy array, dense data at indices, same length as indices and dtype as
    rle_data
    """
    return brle_gatherer_1d(indices)(brle_data)


def brle_reverse(brle_data):
    """Equivalent to dense_to_brle(brle_to_dense(brle_data)[-1::-1])."""
    if len(brle_data) % 2 == 0:
        brle_data = np.concatenate([brle_data, [0]], axis=0)
    end = -1 if brle_data[-1] == 0 else None
    return brle_data[-1:end:-1]


def rle_reverse(rle_data):
    """Get the rle encoding of the reversed dense array."""
    if not isinstance(rle_data, np.ndarray):
        rle_data = np.array(rle_data, copy=False)
    rle_data = np.reshape(rle_data, (-1, 2))
    rle_data = rle_data[-1::-1]
    return np.reshape(rle_data, (-1,))


def rle_to_sparse(rle_data):
    """Get dense indices associated with non-zeros."""
    indices = []
    values = []
    it = iter(rle_data)
    index = 0
    try:
        while True:
            value = next(it)
            counts = next(it)
            end = index + counts
            if value:
                indices.append(np.arange(index, end, dtype=np.int64))
                values.append(np.repeat(value, counts))
            index = end
    except StopIteration:
        pass
    if len(indices) == 0:
        assert len(values) == 0
        return indices, values

    indices = np.concatenate(indices)
    values = np.concatenate(values)
    return indices, values


def brle_to_sparse(brle_data, dtype=np.int64):
    ends = np.cumsum(brle_data)
    indices = [np.arange(s, e, dtype=dtype) for s, e in
               zip(ends[::2], ends[1::2])]
    return np.concatenate(indices)


def rle_strip(rle_data):
    """
    Remove leading and trailing zeros.

    Parameters
    ----------
    rle_data: run length encoded data

    Returns
    ----------
    (stripped_rle_data, padding)
        stripped_rle_data: rle data without any leading or trailing zeros
        padding: 2-element dense padding
    """
    rle_data = np.reshape(rle_data, (-1, 2))
    start = 0
    final_i = len(rle_data)
    for i, (val, count) in enumerate(rle_data):
        if val and count > 0:
            final_i = i
            break
        else:
            start += count

    end = 0
    final_j = len(rle_data)
    for j, (val, count) in enumerate(rle_data[::-1]):
        if val and count > 0:
            final_j = j
            break
        else:
            end += count

    rle_data = rle_data[
        final_i:None if final_j == 0 else -final_j].reshape((-1,))
    return rle_data, (start, end)


def brle_strip(brle_data):
    """
    Remove leading and trailing zeros.

    Parameters
    ----------
    brle_data: binary run length encoded data.

    Returns
    ----------
    (stripped_brle_data, padding)
    stripped_brle_data: rle data without any leading or trailing zeros
    padding: 2-element dense padding
    """
    start = 0
    val = True
    final_i = len(brle_data)
    for i, count in enumerate(brle_data):
        val = not val
        if val and count > 0:
            final_i = i
            break
        else:
            start += count
    end = 0
    final_j = len(brle_data)
    val = bool(len(brle_data) % 2)
    for j, count in enumerate(brle_data[::-1]):
        val = not val
        if val and count > 0:
            final_j = j
            break
        else:
            end += count

    brle_data = brle_data[final_i:None if final_j == 0 else -final_j]
    brle_data = np.concatenate([[0], brle_data])
    return brle_data, (start, end)


# trimesh/voxel/transforms.py

import numpy as np

from .. import util
from .. import caching

from .. import transformations as tr


class Transform(object):
    """
    Class for caching metadata associated with 4x4 transformations.

    The transformation matrix is used to define relevant properties
    for the voxels, including pitch and origin.
    """

    def __init__(self, matrix):
        """
        Initialize with a transform

        Parameters
        -----------
        matrix : (4, 4) float
          Homogeneous transformation matrix
        """
        matrix = np.asanyarray(matrix, dtype=np.float64)
        if matrix.shape != (4, 4):
            raise ValueError('matrix must be 4x4!')

        if not np.all(matrix[3, :] == [0, 0, 0, 1]):
            raise ValueError('matrix not a valid transformation matrix')

        # store matrix as data
        self._data = caching.tracked_array(matrix, dtype=np.float64)
        # dump cache when matrix changes
        self._cache = caching.Cache(
            id_function=self._data.__hash__)

    def crc(self):
        util.log.warning(
            '`geometry.crc()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def hash(self):
        util.log.warning(
            '`geometry.hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`')
        return self.__hash__()

    def __hash__(self):
        """
        Get the hash of the current transformation matrix.

        Returns
        ------------
        hash : str
          Hash of transformation matrix
        """
        return self._data.__hash__()

    @property
    def translation(self):
        """
        Get the translation component of the matrix

        Returns
        ------------
        translation : (3,) float
          Cartesian translation
        """
        return self._data[:3, 3]

    @property
    def matrix(self):
        """
        Get the homogeneous transformation matrix.

        Returns
        -------------
        matrix : (4, 4) float
          Transformation matrix
        """
        return self._data

    @matrix.setter
    def matrix(self, data):
        """
        Set the homogeneous transformation matrix.

        Parameters
        -------------
        matrix : (4, 4) float
          Transformation matrix
        """
        data = np.asanyarray(data, dtype=np.float64)
        if data.shape != (4, 4):
            raise ValueError('matrix must be (4, 4)!')
        self._data = caching.tracked_array(data, dtype=np.float64)

    @caching.cache_decorator
    def scale(self):
        """
        Get the scale factor of the current transformation.

        Returns
        -------------
        scale : (3,) float
          Scale factor from the matrix
        """
        # get the current transformation
        matrix = self.matrix
        # get the (3,) diagonal of the rotation component
        scale = np.diag(matrix[:3, :3])
        if not np.allclose(
                matrix[:3, :3],
                scale * np.eye(3),
                scale * 1e-6 + 1e-8):
            raise RuntimeError('transform features a shear or rotation')
        return scale

    @caching.cache_decorator
    def pitch(self):
        scale = self.scale
        if not util.allclose(
                scale[0], scale[1:],
                np.max(np.abs(scale)) * 1e-6 + 1e-8):
            raise RuntimeError('transform features non-uniform scaling')
        return scale

    @caching.cache_decorator
    def unit_volume(self):
        """Volume of a transformed unit cube."""
        return np.linalg.det(self._data[:3, :3])

    def apply_transform(self, matrix):
        """Mutate the transform in-place and return self."""
        self.matrix = np.matmul(matrix, self.matrix)
        return self

    def apply_translation(self, translation):
        """Mutate the transform in-place and return self."""
        self.matrix[:3, 3] += translation
        return self

    def apply_scale(self, scale):
        """Mutate the transform in-place and return self."""
        self.matrix[:3] *= scale
        return self

    def transform_points(self, points):
        """
        Apply the transformation to points (not in-place).

        Parameters
        ----------
        points: (n, 3) float
          Points in cartesian space

        Returns
        ----------
        transformed : (n, 3) float
          Points transformed by matrix
        """
        if self.is_identity:
            return points.copy()
        return tr.transform_points(
            points.reshape(-1, 3), self.matrix).reshape(points.shape)

    def inverse_transform_points(self, points):
        """Apply the inverse transformation to points (not in-place)."""
        if self.is_identity:
            return points
        return tr.transform_points(
            points.reshape(-1, 3),
            self.inverse_matrix).reshape(points.shape)

    @caching.cache_decorator
    def inverse_matrix(self):
        inv = np.linalg.inv(self.matrix)
        inv.flags.writeable = False
        return inv

    def copy(self):
        return Transform(self._data.copy())

    @caching.cache_decorator
    def is_identity(self):
        """
        Flags this transformation being sufficiently close to eye(4).
        """
        return util.allclose(self.matrix, np.eye(4), 1e-8)


# setup.py

#!/usr/bin/env python
# ruff: noqa
import os
import sys
from setuptools import setup

# load __version__ without importing anything
_version_file = os.path.join(
    os.path.dirname(__file__),
    'trimesh', 'version.py')

if os.path.isfile(_version_file):
    with open(_version_file, 'r') as f:
        _version_raw = f.read()
    # use eval to get a clean string of version from file
    __version__ = eval(next(
        line.strip().split('=')[-1]
        for line in str.splitlines(_version_raw)
        if '_version_' in line))
else:
    __version__ = None

# load README.md as long_description
long_description = ''
if os.path.exists('README.md'):
    with open('README.md', 'r') as f:
        long_description = f.read()

# minimal requirements for installing trimesh
requirements_default = set(['numpy'])

# "easy" requirements should install without compiling
# anything on Windows, Linux, and Mac, for Python >= 3.6
requirements_easy = set([
    'scipy',     # provide convex hulls, fast graph ops, etc
    'networkx',  # provide slow graph ops with a nice API
    'lxml',      # handle XML better and faster than built- in XML
    'shapely',   # handle 2D polygons robustly
    'rtree',     # create N-dimension trees for broad-phase queries
    'svg.path',  # handle SVG format path strings
    'sympy',     # do analytical math
    'pillow',    # load images
    'embreex',   # Intel's Embree ray check engine with wheels
    'requests',  # do network requests
    'xxhash',    # hash ndarrays faster than built-in MD5/CRC
    'setuptools',  # do setuptools stuff
    'jsonschema',  # validate JSON schemas like GLTF
    'pycollada',   # parse collada/dae/zae files
    'chardet',     # figure out if someone used UTF-16
    'mapbox-earcut',  # fast 2D triangulations of polygons
    'colorlog'])   # log in pretty colors

# "all" requirements only need to be installable
# through some mechanism on Linux with Python 3.5+
# and are allowed to compile code
requirements_all = requirements_easy.union([
    'python-fcl',    # do fast 3D collision queries
    'psutil',        # figure out how much memory we have
    'glooey',        # make GUI applications with 3D stuff
    'meshio',        # load a number of additional mesh formats; Python 3.5+
    'scikit-image',  # marching cubes and other nice stuff
    'xatlas',        # texture unwrapping
    'pyglet<2',  # render preview windows nicely : note pyglet 2.0 is basically a re-write
])
# requirements for running unit tests
requirements_test = set(['pytest',       # run all unit tests
                         'pytest-cov',   # coverage plugin
                         'pyinstrument',  # profile code
                         'coveralls',    # report coverage stats
                         'autopep8<2',     # check and autoformat
                         'ruff',         # static code analysis
                         'pymeshlab',    # used as a validator for exports
                         'ezdxf'])       # used as a validator for exports

# Python 2.7 and 3.4 support has been dropped from packages
# version lock those packages here so install succeeds
current = (sys.version_info.major, sys.version_info.minor)

# packages that no longer support old Python
lock = [((3, 4), 'lxml', '4.3.5'),
        ((3, 4), 'shapely', '1.6.4'),
        ((3, 4), 'pyglet', '1.4.10'),
        ((3, 5), 'sympy', None),
        ((3, 6), 'pyglet<2', None),
        ((3, 6), 'autopep8', None),
        ((3, 6), 'ruff', None),
        ((3, 7), 'pymeshlab', None),
        ((3, 5), 'embreex', None),
        ((3, 6), 'svg.path', '4.1')]
for max_python, name, version in lock:
    if current <= max_python:
        # remove version-free requirements
        requirements_easy.discard(name)
        requirements_test.discard(name)

        # if version is None drop that package
        if version is not None:
            # add working version locked requirements
            requirements_easy.add('{}=={}'.format(name, version))


def format_all():
    """
    A shortcut to run automatic formatting and complaining
    on all of the trimesh subdirectories.
    """
    import subprocess

    def run_on(target):
        # words that codespell hates
        # note that it always checks against the lower case
        word_skip = "datas,coo,nd,files',filetests,ba,childs,whats"
        # files to skip spelling on
        file_skip = "*.pyc,*.zip,.DS_Store,*.js,./trimesh/resources"
        spell = ['codespell', '-i', '3',
                 '--skip=' + file_skip,
                 '-L', word_skip, '-w', target]
        print("Running: \n {} \n\n\n".format(' '.join(spell)))
        subprocess.check_call(spell)

        formatter = ["autopep8", "--recursive", "--verbose",
                     "--in-place", "--aggressive", target]
        print("Running: \n {} \n\n\n".format(
            ' '.join(formatter)))
        subprocess.check_call(formatter)

        flake = ['flake8', target]
        print("Running: \n {} \n\n\n".format(' '.join(flake)))
        subprocess.check_call(flake)

    # run on our target locations
    for t in ['trimesh', 'tests', 'examples']:
        run_on(t)


# if someone wants to output a requirements file
# `python setup.py --list-all > requirements.txt`
if '--list-all' in sys.argv:
    # will not include default requirements (numpy)
    print('\n'.join(requirements_all))
    exit()
elif '--list-easy' in sys.argv:
    # again will not include numpy+setuptools
    print('\n'.join(requirements_easy))
    exit()
elif '--list-test' in sys.argv:
    # again will not include numpy+setuptools
    print('\n'.join(requirements_test))
    exit()
elif '--format' in sys.argv:
    format_all()
    exit()
elif '--bump' in sys.argv:
    # bump the version number
    # convert current version to integers
    bumped = [int(i) for i in __version__.split('.')]
    # increment the last field by one
    bumped[-1] += 1
    # re-combine into a version string
    version_new = '.'.join(str(i) for i in bumped)
    print('version bump `{}` => `{}`'.format(
        __version__, version_new))
    # write back the original version file with
    # just the value replaced with the new one
    raw_new = _version_raw.replace(__version__, version_new)
    with open(_version_file, 'w') as f:
        f.write(raw_new)
    exit()


# call the magical setuptools setup
setup(name='trimesh',
      version=__version__,
      description='Import, export, process, analyze and view triangular meshes.',
      long_description=long_description,
      long_description_content_type='text/markdown',
      author='Michael Dawson-Haggerty',
      author_email='mikedh@kerfed.com',
      license='MIT',
      url='https://github.com/mikedh/trimesh',
      keywords='graphics mesh geometry 3D',
      classifiers=[
          'Development Status :: 4 - Beta',
          'License :: OSI Approved :: MIT License',
          'Programming Language :: Python',
          'Programming Language :: Python :: 2.7',
          'Programming Language :: Python :: 3.5',
          'Programming Language :: Python :: 3.6',
          'Programming Language :: Python :: 3.7',
          'Programming Language :: Python :: 3.8',
          'Programming Language :: Python :: 3.9',
          'Programming Language :: Python :: 3.10',
          'Natural Language :: English',
          'Topic :: Scientific/Engineering'],
      packages=[
          'trimesh',
          'trimesh.ray',
          'trimesh.path',
          'trimesh.path.exchange',
          'trimesh.scene',
          'trimesh.voxel',
          'trimesh.visual',
          'trimesh.viewer',
          'trimesh.exchange',
          'trimesh.resources',
          'trimesh.interfaces'],
      package_data={'trimesh': ['resources/templates/*',
                                'resources/*.json',
                                'resources/schema/*',
                                'resources/schema/primitive/*.json',
                                'resources/*.zip']},
      install_requires=list(requirements_default),
      extras_require={'test': list(requirements_test),
                      'easy': list(requirements_easy),
                      'all': list(requirements_all)})


# trimesh/__init__.py

"""
https://github.com/mikedh/trimesh
------------------------------------

Trimesh is a pure Python (2.7- 3.3+) library for loading and using triangular
meshes with an emphasis on watertight meshes. The goal of the library is to
provide a fully featured Trimesh object which allows for easy manipulation
and analysis, in the style of the Polygon object in the Shapely library.
"""

# current version
from .version import __version__

# geometry objects
from .base import Trimesh
from .points import PointCloud
from .scene.scene import Scene

# utility functions
from .util import unitize
from .transformations import transform_points

# general numeric tolerances
from .constants import tol

# loader functions
from .exchange.load import (
    load,
    load_mesh,
    load_path,
    load_remote,
    available_formats)

# avoid a circular import in trimesh.base
from . import voxel
from . import bounds
from . import nsphere
from . import collision
from . import smoothing
from . import primitives

try:
    # handle vector paths
    from . import path
except BaseException as E:
    # raise a useful error if path hasn't loaded
    from .exceptions import ExceptionWrapper
    path = ExceptionWrapper(E)

# explicitly list imports in __all__
# as otherwise flake8 gets mad
__all__ = [__version__,
           'Trimesh',
           'PointCloud',
           'Scene',
           'voxel',
           'unitize',
           'bounds',
           'nsphere',
           'collision',
           'smoothing',
           'tol',
           'path',
           'load',
           'load_mesh',
           'load_path',
           'load_remote',
           'primitives',
           'transform_points',
           'available_formats']


# trimesh/base.py

"""
github.com/mikedh/trimesh
----------------------------

Library for importing, exporting and doing simple operations on triangular meshes.
"""

from . import ray
from . import util
from . import units
from . import poses
from . import graph
from . import sample
from . import repair
from . import convex
from . import remesh
from . import caching
from . import inertia
from . import boolean
from . import grouping
from . import geometry
from . import permutate
from . import proximity
from . import triangles
from . import curvature
from . import smoothing  # noqa
from . import comparison
from . import registration
from . import decomposition
from . import intersections
from . import transformations

from .visual import create_visual, TextureVisuals
from .exchange.export import export_mesh
from .constants import log, log_time, tol

from .scene import Scene
from .parent import Geometry3D

import copy
import warnings
import numpy as np


class Trimesh(Geometry3D):

    def __init__(self,
                 vertices=None,
                 faces=None,
                 face_normals=None,
                 vertex_normals=None,
                 face_colors=None,
                 vertex_colors=None,
                 face_attributes=None,
                 vertex_attributes=None,
                 metadata=None,
                 process=True,
                 validate=False,
                 merge_tex=None,
                 merge_norm=None,
                 use_embree=True,
                 initial_cache=None,
                 visual=None,
                 **kwargs):
        """
        A Trimesh object contains a triangular 3D mesh.

        Parameters
        ------------
        vertices : (n, 3) float
          Array of vertex locations
        faces : (m, 3) or (m, 4) int
          Array of triangular or quad faces (triangulated on load)
        face_normals : (m, 3) float
          Array of normal vectors corresponding to faces
        vertex_normals : (n, 3) float
          Array of normal vectors for vertices
        metadata : dict
          Any metadata about the mesh
        process : bool
          if True, Nan and Inf values will be removed
          immediately and vertices will be merged
        validate : bool
          If True, degenerate and duplicate faces will be
          removed immediately, and some functions will alter
          the mesh to ensure consistent results.
        use_embree : bool
          If True try to use pyembree raytracer.
          If pyembree is not available it will automatically fall
          back to a much slower rtree/numpy implementation
        initial_cache : dict
          A way to pass things to the cache in case expensive
          things were calculated before creating the mesh object.
        visual : ColorVisuals or TextureVisuals
          Assigned to self.visual
        """

        if initial_cache is None:
            initial_cache = {}

        # self._data stores information about the mesh which
        # CANNOT be regenerated.
        # in the base class all that is stored here is vertex and
        # face information
        # any data put into the store is converted to a TrackedArray
        # which is a subclass of np.ndarray that provides hash and crc
        # methods which can be used to detect changes in the array.
        self._data = caching.DataStore()

        # self._cache stores information about the mesh which CAN be
        # regenerated from self._data, but may be slow to calculate.
        # In order to maintain consistency
        # the cache is cleared when self._data.__hash__() changes
        self._cache = caching.Cache(
            id_function=self._data.__hash__,
            force_immutable=True)
        self._cache.update(initial_cache)

        # check for None only to avoid warning messages in subclasses
        if vertices is not None:
            # (n, 3) float, set of vertices
            self.vertices = vertices
        if faces is not None:
            # (m, 3) int of triangle faces, references self.vertices
            self.faces = faces

        # hold visual information about the mesh (vertex and face colors)
        if visual is None:
            self.visual = create_visual(
                face_colors=face_colors,
                vertex_colors=vertex_colors,
                mesh=self)
        else:
            self.visual = visual

        # normals are accessed through setters/properties and are regenerated
        # if dimensions are inconsistent, but can be set by the constructor
        # to avoid a substantial number of cross products
        if face_normals is not None:
            self.face_normals = face_normals

        # (n, 3) float of vertex normals, can be created from face normals
        if vertex_normals is not None:
            self.vertex_normals = vertex_normals

        # embree is a much, much faster raytracer written by Intel
        # if you have pyembree installed you should use it
        # although both raytracers were designed to have a common API
        if ray.has_embree and use_embree:
            self.ray = ray.ray_pyembree.RayMeshIntersector(self)
        else:
            # create a ray-mesh query object for the current mesh
            # initializing is very inexpensive and object is convenient to have.
            # On first query expensive bookkeeping is done (creation of r-tree),
            # and is cached for subsequent queries
            self.ray = ray.ray_triangle.RayMeshIntersector(self)

        # a quick way to get permuted versions of the current mesh
        self.permutate = permutate.Permutator(self)

        # convenience class for nearest point queries
        self.nearest = proximity.ProximityQuery(self)

        # store metadata about the mesh in a dictionary
        self.metadata = dict()
        # update the mesh metadata with passed metadata
        if isinstance(metadata, dict):
            self.metadata.update(metadata)
        elif metadata is not None:
            raise ValueError(
                'metadata should be a dict or None, got %s' % str(metadata))

        # Set the default center of mass and density
        self._density = 1.0
        self._center_mass = None

        # store per-face and per-vertex attributes which will
        # be updated when an update_faces call is made
        self.face_attributes = {}
        self.vertex_attributes = {}
        # use update to copy items
        if face_attributes is not None:
            self.face_attributes.update(face_attributes)
        if vertex_attributes is not None:
            self.vertex_attributes.update(vertex_attributes)

        # process will remove NaN and Inf values and merge vertices
        # if validate, will remove degenerate and duplicate faces
        if process or validate:
            self.process(validate=validate,
                         merge_tex=merge_tex,
                         merge_norm=merge_norm)

        # save reference to kwargs
        self._kwargs = kwargs

    def process(self,
                validate=False,
                merge_tex=None,
                merge_norm=None):
        """
        Do processing to make a mesh useful.

        Does this by:
            1) removing NaN and Inf values
            2) merging duplicate vertices
        If validate:
            3) Remove triangles which have one edge
               of their 2D oriented bounding box
               shorter than tol.merge
            4) remove duplicated triangles
            5) ensure triangles are consistently wound
               and normals face outwards

        Parameters
        ------------
        validate : bool
          Remove degenerate and duplicate faces.

        Returns
        ------------
        self: trimesh.Trimesh
          Current mesh
        """
        # if there are no vertices or faces exit early
        if self.is_empty:
            return self

        # avoid clearing the cache during operations
        with self._cache:
            # if we're cleaning remove duplicate
            # and degenerate faces
            if validate:
                # get a mask with only unique and non-degenerate faces
                mask = self.unique_faces() & self.nondegenerate_faces()
                self.fix_normals()
                self.update_faces(mask)

            # since none of our process operations moved vertices or faces
            # we can keep face and vertex normals in the cache without recomputing
            # if faces or vertices have been removed, normals are validated before
            # being returned so there is no danger of inconsistent dimensions
            self.remove_infinite_values()
            self.merge_vertices(merge_tex=merge_tex,
                                merge_norm=merge_norm)
            self._cache.clear(exclude={'face_normals',
                                       'vertex_normals'})

        self.metadata['processed'] = True
        return self

    @property
    def faces(self):
        """
        The faces of the mesh.

        This is regarded as core information which cannot be
        regenerated from cache and as such is stored in
        `self._data` which tracks the array for changes and
        clears cached values of the mesh altered.

        Returns
        ----------
        faces : (n, 3) int64
          References for `self.vertices` for triangles.
        """
        return self._data.get(
            'faces', np.empty(shape=(0, 3), dtype=np.int64))

    @faces.setter
    def faces(self, values):
        """
        Set the vertex indexes that make up triangular faces.

        Parameters
        --------------
        values : (n, 3) int64
          Indexes of self.vertices
        """
        if values is None or len(values) == 0:
            return self._data.data.pop('faces', None)
        if not (isinstance(values, np.ndarray) and values.dtype == np.int64):
            values = np.asanyarray(values, dtype=np.int64)

        # automatically triangulate quad faces
        if len(values.shape) == 2 and values.shape[1] != 3:
            log.info('triangulating faces')
            values = geometry.triangulate_quads(values)
        self._data['faces'] = values

    @caching.cache_decorator
    def faces_sparse(self):
        """
        A sparse matrix representation of the faces.

        Returns
        ----------
        sparse : scipy.sparse.coo_matrix
          Has properties:
          dtype : bool
          shape : (len(self.vertices), len(self.faces))
        """
        sparse = geometry.index_sparse(
            columns=len(self.vertices),
            indices=self.faces)
        return sparse

    @property
    def face_normals(self):
        """
        Return the unit normal vector for each face.

        If a face is degenerate and a normal can't be generated
        a zero magnitude unit vector will be returned for that face.

        Returns
        -----------
        normals : (len(self.faces), 3) np.float64
          Normal vectors of each face
        """
        # check shape of cached normals
        cached = self._cache['face_normals']
        # get faces from datastore
        if 'faces' in self._data:
            faces = self._data.data['faces']
        else:
            faces = None

        # if we have no faces exit early
        if faces is None or len(faces) == 0:
            return np.array([], dtype=np.int64).reshape((0, 3))

        # if the shape of cached normals equals the shape of faces return
        if np.shape(cached) == np.shape(faces):
            return cached

        # use cached triangle cross products to generate normals
        # this will always return the correct shape but some values
        # will be zero or an arbitrary vector if the inputs had
        # a cross product below machine epsilon
        normals, valid = triangles.normals(
            triangles=self.triangles,
            crosses=self.triangles_cross)

        # if all triangles are valid shape is correct
        if valid.all():
            # put calculated face normals into cache manually
            self._cache['face_normals'] = normals
            return normals

        # make a padded list of normals for correct shape
        padded = np.zeros((len(self.triangles), 3),
                          dtype=np.float64)
        padded[valid] = normals

        # put calculated face normals into cache manually
        self._cache['face_normals'] = padded

        return padded

    @face_normals.setter
    def face_normals(self, values):
        """
        Assign values to face normals.

        Parameters
        -------------
        values : (len(self.faces), 3) float
          Unit face normals
        """
        # if nothing passed exit
        if values is None:
            return
        # make sure candidate face normals are C-contiguous float
        values = np.asanyarray(
            values, order='C', dtype=np.float64)
        # face normals need to correspond to faces
        if len(values) == 0 or values.shape != self.faces.shape:
            log.debug('face_normals incorrect shape, ignoring!')
            return
        # check if any values are larger than tol.merge
        # don't set the normals if they are all zero
        ptp = values.ptp()
        if not np.isfinite(ptp):
            log.debug('face_normals contain NaN, ignoring!')
            return
        if ptp < tol.merge:
            log.debug('face_normals all zero, ignoring!')
            return

        # make sure the first few normals match the first few triangles
        check, valid = triangles.normals(
            self.vertices.view(np.ndarray)[self.faces[:20]])
        compare = np.zeros((len(valid), 3))
        compare[valid] = check
        if not np.allclose(compare, values[:20]):
            log.debug("face_normals didn't match triangles, ignoring!")
            return
        # otherwise store face normals
        self._cache['face_normals'] = values

    @property
    def vertices(self):
        """
        The vertices of the mesh.

        This is regarded as core information which cannot be
        generated from cache and as such is stored in self._data
        which tracks the array for changes and clears cached
        values of the mesh if this is altered.

        Returns
        ----------
        vertices : (n, 3) float
          Points in cartesian space referenced by self.faces
        """
        return self._data.get('vertices', np.empty(
            shape=(0, 3), dtype=np.float64))

    @vertices.setter
    def vertices(self, values):
        """
        Assign vertex values to the mesh.

        Parameters
        --------------
        values : (n, 3) float
          Points in space
        """
        self._data['vertices'] = np.asanyarray(
            values, order='C', dtype=np.float64)

    @caching.cache_decorator
    def vertex_normals(self):
        """
        The vertex normals of the mesh. If the normals were loaded
        we check to make sure we have the same number of vertex
        normals and vertices before returning them. If there are
        no vertex normals defined or a shape mismatch we  calculate
        the vertex normals from the mean normals of the faces the
        vertex is used in.

        Returns
        ----------
        vertex_normals : (n, 3) float
          Represents the surface normal at each vertex.
          Where n == len(self.vertices)
        """
        # make sure we have faces_sparse
        assert hasattr(self.faces_sparse, 'dot')
        vertex_normals = geometry.weighted_vertex_normals(
            vertex_count=len(self.vertices),
            faces=self.faces,
            face_normals=self.face_normals,
            face_angles=self.face_angles)
        return vertex_normals

    @vertex_normals.setter
    def vertex_normals(self, values):
        """
        Assign values to vertex normals.

        Parameters
        -------------
        values : (len(self.vertices), 3) float
          Unit normal vectors for each vertex
        """
        if values is not None:
            values = np.asanyarray(values,
                                   order='C',
                                   dtype=np.float64)
            if values.shape == self.vertices.shape:
                # check to see if they assigned all zeros
                if values.ptp() < tol.merge:
                    log.debug('vertex_normals are all zero!')
                self._cache['vertex_normals'] = values

    @caching.cache_decorator
    def vertex_faces(self):
        """
        A representation of the face indices that correspond to each vertex.

        Returns
        ----------
        vertex_faces : (n,m) int
          Each row contains the face indices that correspond to the given vertex,
          padded with -1 up to the max number of faces corresponding to any one vertex
          Where n == len(self.vertices), m == max number of faces for a single vertex
        """
        vertex_faces = geometry.vertex_face_indices(
            vertex_count=len(self.vertices),
            faces=self.faces,
            faces_sparse=self.faces_sparse)
        return vertex_faces

    @caching.cache_decorator
    def bounds(self):
        """
        The axis aligned bounds of the faces of the mesh.

        Returns
        -----------
        bounds : (2, 3) float or None
          Bounding box with [min, max] coordinates
          If mesh is empty will return None
        """
        # return bounds including ONLY referenced vertices
        in_mesh = self.vertices[self.referenced_vertices]
        # don't crash if we have no vertices referenced
        if len(in_mesh) == 0:
            return None
        # get mesh bounds with min and max
        return np.array([in_mesh.min(axis=0),
                         in_mesh.max(axis=0)])

    @caching.cache_decorator
    def extents(self):
        """
        The length, width, and height of the axis aligned
        bounding box of the mesh.

        Returns
        -----------
        extents : (3, ) float or None
          Array containing axis aligned [length, width, height]
          If mesh is empty returns None
        """
        # if mesh is empty return None
        if self.bounds is None:
            return None
        extents = self.bounds.ptp(axis=0)

        return extents

    @caching.cache_decorator
    def scale(self):
        """
        A metric for the overall scale of the mesh, the length of the
        diagonal of the axis aligned bounding box of the mesh.

        Returns
        ----------
        scale : float
          The length of the meshes AABB diagonal
        """
        # if mesh is empty just return no scale
        if self.extents is None:
            return 1.0
        # make sure we are returning python floats
        scale = float((self.extents ** 2).sum() ** .5)
        return scale

    @caching.cache_decorator
    def centroid(self):
        """
        The point in space which is the average of the triangle
        centroids weighted by the area of each triangle.

        This will be valid even for non-watertight meshes,
        unlike self.center_mass

        Returns
        ----------
        centroid : (3, ) float
          The average vertex weighted by face area
        """

        # use the centroid of each triangle weighted by
        # the area of the triangle to find the overall centroid
        try:
            centroid = np.average(self.triangles_center,
                                  weights=self.area_faces,
                                  axis=0)
        except BaseException:
            # if all triangles are zero-area weights will not work
            centroid = self.triangles_center.mean(axis=0)
        return centroid

    @property
    def center_mass(self):
        """
        The point in space which is the center of mass/volume.

        If the current mesh is not watertight this is meaningless
        garbage unless it was explicitly set.

        Returns
        -----------
        center_mass : (3, ) float
           Volumetric center of mass of the mesh
        """
        center_mass = self.mass_properties['center_mass']
        return center_mass

    @center_mass.setter
    def center_mass(self, cm):
        self._center_mass = cm
        self._cache.delete('mass_properties')

    @property
    def density(self):
        """
        The density of the mesh.

        Returns
        -----------
        density : float
          The density of the mesh.
        """
        density = self.mass_properties['density']
        return density

    @density.setter
    def density(self, value):
        """
        Set the density of the mesh.

        Parameters
        -------------
        density : float
          Specify the density of the mesh to be
          used in inertia calculations.
        """
        self._density = float(value)
        self._cache.delete('mass_properties')

    @property
    def volume(self):
        """
        Volume of the current mesh calculated using a surface
        integral. If the current mesh isn't watertight this is
        garbage.

        Returns
        ---------
        volume : float
          Volume of the current mesh
        """
        volume = self.mass_properties['volume']
        return volume

    @property
    def mass(self):
        """
        Mass of the current mesh, based on specified density and
        volume. If the current mesh isn't watertight this is garbage.

        Returns
        ---------
        mass : float
          Mass of the current mesh
        """
        mass = self.mass_properties['mass']
        return mass

    @property
    def moment_inertia(self):
        """
        Return the moment of inertia matrix of the current mesh.
        If mesh isn't watertight this is garbage. The returned
        moment of inertia is *axis aligned* at the mesh's center
        of mass `mesh.center_mass`. If you want the moment at any
        other frame including the origin call:
        `mesh.moment_inertia_frame`

        Returns
        ---------
        inertia : (3, 3) float
          Moment of inertia of the current mesh at the center of
          mass and aligned with the cartesian axis.
        """
        inertia = self.mass_properties['inertia']
        return inertia

    def moment_inertia_frame(self, transform):
        """
        Get the moment of inertia of this mesh with respect to
        an arbitrary frame, versus with respect to the center
        of mass as returned by `mesh.moment_inertia`.

        For example if `transform` is an identity matrix `np.eye(4)`
        this will give the moment at the origin.

        Uses the parallel axis theorum to move the center mass
        tensor to this arbitrary frame.

        Parameters
        ------------
        transform : (4, 4) float
          Homogeneous transformation matrix.

        Returns
        -------------
        inertia : (3, 3)
          Moment of inertia in the requested frame.
        """
        # we'll need the inertia tensor and the center of mass
        props = self.mass_properties
        # calculated moment of inertia is at the center of mass
        # so we want to offset our requested translation by that
        # center of mass
        offset = np.eye(4)
        offset[:3, 3] = -props['center_mass']

        # apply the parallel axis theorum to get the new inertia
        return inertia.transform_inertia(
            inertia_tensor=props['inertia'],
            transform=np.dot(offset, transform),
            mass=props['mass'],
            parallel_axis=True)

    @caching.cache_decorator
    def principal_inertia_components(self):
        """
        Return the principal components of inertia

        Ordering corresponds to mesh.principal_inertia_vectors

        Returns
        ----------
        components : (3, ) float
          Principal components of inertia
        """
        # both components and vectors from inertia matrix
        components, vectors = inertia.principal_axis(self.moment_inertia)
        # store vectors in cache for later
        self._cache['principal_inertia_vectors'] = vectors

        return components

    @property
    def principal_inertia_vectors(self):
        """
        Return the principal axis of inertia as unit vectors.
        The order corresponds to `mesh.principal_inertia_components`.

        Returns
        ----------
        vectors : (3, 3) float
          Three vectors pointing along the
          principal axis of inertia directions
        """
        _ = self.principal_inertia_components
        return self._cache['principal_inertia_vectors']

    @caching.cache_decorator
    def principal_inertia_transform(self):
        """
        A transform which moves the current mesh so the principal
        inertia vectors are on the X,Y, and Z axis, and the centroid is
        at the origin.

        Returns
        ----------
        transform : (4, 4) float
          Homogeneous transformation matrix
        """
        order = np.argsort(self.principal_inertia_components)[1:][::-1]
        vectors = self.principal_inertia_vectors[order]
        vectors = np.vstack((vectors, np.cross(*vectors)))

        transform = np.eye(4)
        transform[:3, :3] = vectors
        transform = transformations.transform_around(
            matrix=transform,
            point=self.centroid)
        transform[:3, 3] -= self.centroid

        return transform

    @caching.cache_decorator
    def symmetry(self):
        """
        Check whether a mesh has rotational symmetry around
        an axis (radial) or point (spherical).

        Returns
        -----------
        symmetry : None, 'radial', 'spherical'
          What kind of symmetry does the mesh have.
        """
        symmetry, axis, section = inertia.radial_symmetry(self)
        self._cache['symmetry_axis'] = axis
        self._cache['symmetry_section'] = section
        return symmetry

    @property
    def symmetry_axis(self):
        """
        If a mesh has rotational symmetry, return the axis.

        Returns
        ------------
        axis : (3, ) float
          Axis around which a 2D profile was revolved to create this mesh.
        """
        if self.symmetry is not None:
            return self._cache['symmetry_axis']

    @property
    def symmetry_section(self):
        """
        If a mesh has rotational symmetry return the two
        vectors which make up a section coordinate frame.

        Returns
        ----------
        section : (2, 3) float
          Vectors to take a section along
        """
        if self.symmetry is not None:
            return self._cache['symmetry_section']

    @caching.cache_decorator
    def triangles(self):
        """
        Actual triangles of the mesh (points, not indexes)

        Returns
        ---------
        triangles : (n, 3, 3) float
          Points of triangle vertices
        """
        # use of advanced indexing on our tracked arrays will
        # trigger a change flag which means the hash will have to be
        # recomputed. We can escape this check by viewing the array.
        triangles = self.vertices.view(np.ndarray)[self.faces]

        return triangles

    @caching.cache_decorator
    def triangles_tree(self):
        """
        An R-tree containing each face of the mesh.

        Returns
        ----------
        tree : rtree.index
          Each triangle in self.faces has a rectangular cell
        """
        tree = triangles.bounds_tree(self.triangles)
        return tree

    @caching.cache_decorator
    def triangles_center(self):
        """
        The center of each triangle (barycentric [1/3, 1/3, 1/3])

        Returns
        ---------
        triangles_center : (len(self.faces), 3) float
          Center of each triangular face
        """
        triangles_center = self.triangles.mean(axis=1)
        return triangles_center

    @caching.cache_decorator
    def triangles_cross(self):
        """
        The cross product of two edges of each triangle.

        Returns
        ---------
        crosses : (n, 3) float
          Cross product of each triangle
        """
        crosses = triangles.cross(self.triangles)
        return crosses

    @caching.cache_decorator
    def edges(self):
        """
        Edges of the mesh (derived from faces).

        Returns
        ---------
        edges : (n, 2) int
          List of vertex indices making up edges
        """
        edges, index = geometry.faces_to_edges(self.faces.view(np.ndarray),
                                               return_index=True)
        self._cache['edges_face'] = index
        return edges

    @caching.cache_decorator
    def edges_face(self):
        """
        Which face does each edge belong to.

        Returns
        ---------
        edges_face : (n, ) int
          Index of self.faces
        """
        _ = self.edges
        return self._cache['edges_face']

    @caching.cache_decorator
    def edges_unique(self):
        """
        The unique edges of the mesh.

        Returns
        ----------
        edges_unique : (n, 2) int
          Vertex indices for unique edges
        """
        unique, inverse = grouping.unique_rows(self.edges_sorted)
        edges_unique = self.edges_sorted[unique]
        # edges_unique will be added automatically by the decorator
        # additional terms generated need to be added to the cache manually
        self._cache['edges_unique_idx'] = unique
        self._cache['edges_unique_inverse'] = inverse
        return edges_unique

    @caching.cache_decorator
    def edges_unique_length(self):
        """
        How long is each unique edge.

        Returns
        ----------
        length : (len(self.edges_unique), ) float
          Length of each unique edge
        """
        vector = np.subtract(*self.vertices[self.edges_unique.T])
        length = util.row_norm(vector)
        return length

    @caching.cache_decorator
    def edges_unique_inverse(self):
        """
        Return the inverse required to reproduce
        self.edges_sorted from self.edges_unique.

        Useful for referencing edge properties:
        mesh.edges_unique[mesh.edges_unique_inverse] == m.edges_sorted

        Returns
        ----------
        inverse : (len(self.edges), ) int
          Indexes of self.edges_unique
        """
        _ = self.edges_unique
        return self._cache['edges_unique_inverse']

    @caching.cache_decorator
    def edges_sorted(self):
        """
        Edges sorted along axis 1

        Returns
        ----------
        edges_sorted : (n, 2)
          Same as self.edges but sorted along axis 1
        """
        edges_sorted = np.sort(self.edges, axis=1)
        return edges_sorted

    @caching.cache_decorator
    def edges_sorted_tree(self):
        """
        A KDTree for mapping edges back to edge index.

        Returns
        ------------
        tree : scipy.spatial.cKDTree
          Tree when queried with edges will return
          their index in mesh.edges_sorted
        """
        from scipy.spatial import cKDTree
        return cKDTree(self.edges_sorted)

    @caching.cache_decorator
    def edges_sparse(self):
        """
        Edges in sparse bool COO graph format where connected
        vertices are True.

        Returns
        ----------
        sparse: (len(self.vertices), len(self.vertices)) bool
          Sparse graph in COO format
        """
        sparse = graph.edges_to_coo(self.edges,
                                    count=len(self.vertices))
        return sparse

    @caching.cache_decorator
    def body_count(self):
        """
        How many connected groups of vertices exist in this mesh.
        Note that this number may differ from result in mesh.split,
        which is calculated from FACE rather than vertex adjacency.

        Returns
        -----------
        count : int
          Number of connected vertex groups
        """
        # labels are (len(vertices), int) OB
        count, labels = graph.csgraph.connected_components(
            self.edges_sparse,
            directed=False,
            return_labels=True)
        self._cache['vertices_component_label'] = labels
        return count

    @caching.cache_decorator
    def faces_unique_edges(self):
        """
        For each face return which indexes in mesh.unique_edges constructs
        that face.

        Returns
        ---------
        faces_unique_edges : (len(self.faces), 3) int
          Indexes of self.edges_unique that
          construct self.faces

        Examples
        ---------
        In [0]: mesh.faces[:2]
        Out[0]:
        TrackedArray([[    1,  6946, 24224],
                      [ 6946,  1727, 24225]])

        In [1]: mesh.edges_unique[mesh.faces_unique_edges[:2]]
        Out[1]:
        array([[[    1,  6946],
                [ 6946, 24224],
                [    1, 24224]],
               [[ 1727,  6946],
                [ 1727, 24225],
                [ 6946, 24225]]])
        """
        # make sure we have populated unique edges
        _ = self.edges_unique
        # we are relying on the fact that edges are stacked in triplets
        result = self._cache['edges_unique_inverse'].reshape((-1, 3))
        return result

    @caching.cache_decorator
    def euler_number(self):
        """
        Return the Euler characteristic (a topological invariant) for the mesh
        In order to guarantee correctness, this should be called after
        remove_unreferenced_vertices

        Returns
        ----------
        euler_number : int
          Topological invariant
        """
        euler = int(self.referenced_vertices.sum() -
                    len(self.edges_unique) +
                    len(self.faces))
        return euler

    @caching.cache_decorator
    def referenced_vertices(self):
        """
        Which vertices in the current mesh are referenced by a face.

        Returns
        -------------
        referenced : (len(self.vertices), ) bool
          Which vertices are referenced by a face
        """
        referenced = np.zeros(len(self.vertices), dtype=bool)
        referenced[self.faces] = True
        return referenced

    @property
    def units(self):
        """
        Definition of units for the mesh.

        Returns
        ----------
        units : str
          Unit system mesh is in, or None if not defined
        """
        if 'units' in self.metadata:
            return self.metadata['units']
        else:
            return None

    @units.setter
    def units(self, value):
        value = str(value).lower()
        self.metadata['units'] = value

    def convert_units(self, desired, guess=False):
        """
        Convert the units of the mesh into a specified unit.

        Parameters
        ------------
        desired : string
          Units to convert to (eg 'inches')
        guess : boolean
          If self.units are not defined should we
          guess the current units of the document and then convert?
        """
        units._convert_units(self, desired, guess)
        return self

    def merge_vertices(
            self,
            merge_tex=None,
            merge_norm=None,
            digits_vertex=None,
            digits_norm=None,
            digits_uv=None):
        """
        Removes duplicate vertices grouped by position and
        optionally texture coordinate and normal.

        Parameters
        -------------
        mesh : Trimesh object
          Mesh to merge vertices on
        merge_tex : bool
          If True textured meshes with UV coordinates will
          have vertices merged regardless of UV coordinates
        merge_norm : bool
          If True, meshes with vertex normals will have
          vertices merged ignoring different normals
        digits_vertex : None or int
          Number of digits to consider for vertex position
        digits_norm : int
          Number of digits to consider for unit normals
        digits_uv : int
          Number of digits to consider for UV coordinates
        """
        grouping.merge_vertices(
            mesh=self,
            merge_tex=merge_tex,
            merge_norm=merge_norm,
            digits_vertex=digits_vertex,
            digits_norm=digits_norm,
            digits_uv=digits_uv)

    def update_vertices(self, mask, inverse=None):
        """
        Update vertices with a mask.

        Parameters
        ------------
        vertex_mask : (len(self.vertices)) bool
          Array of which vertices to keep
        inverse : (len(self.vertices)) int
          Array to reconstruct vertex references
          such as output by np.unique
        """
        # if the mesh is already empty we can't remove anything
        if self.is_empty:
            return

        # make sure mask is a numpy array
        mask = np.asanyarray(mask)

        if ((mask.dtype.name == 'bool' and mask.all()) or
                len(mask) == 0 or self.is_empty):
            # mask doesn't remove any vertices so exit early
            return

        # create the inverse mask if not passed
        if inverse is None:
            inverse = np.zeros(len(self.vertices), dtype=np.int64)
            if mask.dtype.kind == 'b':
                inverse[mask] = np.arange(mask.sum())
            elif mask.dtype.kind == 'i':
                inverse[mask] = np.arange(len(mask))
            else:
                inverse = None

        # re-index faces from inverse
        if inverse is not None and util.is_shape(self.faces, (-1, 3)):
            self.faces = inverse[self.faces.reshape(-1)].reshape((-1, 3))

        # update the visual object with our mask
        self.visual.update_vertices(mask)
        # get the normals from cache before dumping
        cached_normals = self._cache['vertex_normals']

        # apply to face_attributes
        count = len(self.vertices)
        for key, value in self.vertex_attributes.items():
            try:
                # covers un-len'd objects as well
                if len(value) != count:
                    raise TypeError()
            except TypeError:
                continue
            # apply the mask to the attribute
            self.vertex_attributes[key] = value[mask]

        # actually apply the mask
        self.vertices = self.vertices[mask]

        # if we had passed vertex normals try to save them
        if util.is_shape(cached_normals, (-1, 3)):
            try:
                self.vertex_normals = cached_normals[mask]
            except BaseException:
                pass

    def update_faces(self, mask):
        """
        In many cases, we will want to remove specific faces.
        However, there is additional bookkeeping to do this cleanly.
        This function updates the set of faces with a validity mask,
        as well as keeping track of normals and colors.

        Parameters
        ------------
        valid : (m) int or (len(self.faces)) bool
          Mask to remove faces
        """
        # if the mesh is already empty we can't remove anything
        if self.is_empty:
            return

        mask = np.asanyarray(mask)
        if mask.dtype.name == 'bool' and mask.all():
            # mask removes no faces so exit early
            return

        # try to save face normals before dumping cache
        cached_normals = self._cache['face_normals']

        faces = self._data['faces']
        # if Trimesh has been subclassed and faces have been moved
        # from data to cache, get faces from cache.
        if not util.is_shape(faces, (-1, 3)):
            faces = self._cache['faces']

        # apply to face_attributes
        count = len(self.faces)
        for key, value in self.face_attributes.items():
            try:
                # covers un-len'd objects as well
                if len(value) != count:
                    raise TypeError()
            except TypeError:

                continue
            # apply the mask to the attribute
            self.face_attributes[key] = value[mask]

        # actually apply the mask
        self.faces = faces[mask]

        # apply to face colors
        self.visual.update_faces(mask)

        # if our normals were the correct shape apply them
        if util.is_shape(cached_normals, (-1, 3)):
            self.face_normals = cached_normals[mask]

    def remove_infinite_values(self):
        """
        Ensure that every vertex and face consists of finite numbers.
        This will remove vertices or faces containing np.nan and np.inf

        Alters `self.faces` and `self.vertices`
        """
        if util.is_shape(self.faces, (-1, 3)):
            # (len(self.faces), ) bool, mask for faces
            face_mask = np.isfinite(self.faces).all(axis=1)
            self.update_faces(face_mask)

        if util.is_shape(self.vertices, (-1, 3)):
            # (len(self.vertices), ) bool, mask for vertices
            vertex_mask = np.isfinite(self.vertices).all(axis=1)
            self.update_vertices(vertex_mask)

    def unique_faces(self):
        """
        On the current mesh find which faces are unique.

        Returns
        --------
        unique : (len(faces),) bool
          A mask where the first occurance of a unique face is true.
        """
        mask = np.zeros(len(self.faces), dtype=bool)
        mask[grouping.unique_rows(np.sort(self.faces, axis=1))[0]] = True
        return mask

    def remove_duplicate_faces(self):
        """
        DERECATED MARCH 2024 REPLACE WITH:
        `mesh.update_faces(mesh.unique_faces())`
        """
        warnings.warn(
            '`remove_duplicate_faces` is deprecated ' +
            'and will be removed in March 2024: ' +
            'replace with `mesh.update_faces(mesh.unique_faces())`',
            category=DeprecationWarning, stacklevel=2)
        self.update_faces(self.unique_faces())

    def rezero(self):
        """
        Translate the mesh so that all vertex vertices are positive.

        Alters `self.vertices`.
        """
        self.apply_translation(self.bounds[0] * -1.0)

    @log_time
    def split(self, **kwargs):
        """
        Returns a list of Trimesh objects, based on face connectivity.
        Splits into individual components, sometimes referred to as 'bodies'

        Parameters
        ------------
        only_watertight : bool
          Only return watertight meshes and discard remainder
        adjacency : None or (n, 2) int
          Override face adjacency with custom values

        Returns
        ---------
        meshes : (n, ) trimesh.Trimesh
          Separate bodies from original mesh
        """
        return graph.split(self, **kwargs)

    @caching.cache_decorator
    def face_adjacency(self):
        """
        Find faces that share an edge i.e. 'adjacent' faces.

        Returns
        ----------
        adjacency : (n, 2) int
          Pairs of faces which share an edge

        Examples
        ---------

        In [1]: mesh = trimesh.load('models/featuretype.STL')

        In [2]: mesh.face_adjacency
        Out[2]:
        array([[   0,    1],
               [   2,    3],
               [   0,    3],
               ...,
               [1112,  949],
               [3467, 3475],
               [1113, 3475]])

        In [3]: mesh.faces[mesh.face_adjacency[0]]
        Out[3]:
        TrackedArray([[   1,    0,  408],
                      [1239,    0,    1]], dtype=int64)

        In [4]: import networkx as nx

        In [5]: graph = nx.from_edgelist(mesh.face_adjacency)

        In [6]: groups = nx.connected_components(graph)
        """
        adjacency, edges = graph.face_adjacency(
            mesh=self, return_edges=True)
        self._cache['face_adjacency_edges'] = edges
        return adjacency

    @caching.cache_decorator
    def face_neighborhood(self):
        """
        Find faces that share a vertex i.e. 'neighbors' faces.

        Returns
        ----------
        neighborhood : (n, 2) int
          Pairs of faces which share a vertex
        """
        return graph.face_neighborhood(self)

    @caching.cache_decorator
    def face_adjacency_edges(self):
        """
        Returns the edges that are shared by the adjacent faces.

        Returns
        --------
        edges : (n, 2) int
           Vertex indices which correspond to face_adjacency
        """
        # this value is calculated as a byproduct of the face adjacency
        _ = self.face_adjacency
        return self._cache['face_adjacency_edges']

    @caching.cache_decorator
    def face_adjacency_edges_tree(self):
        """
        A KDTree for mapping edges back face adjacency index.

        Returns
        ------------
        tree : scipy.spatial.cKDTree
          Tree when queried with SORTED edges will return
          their index in mesh.face_adjacency
        """
        from scipy.spatial import cKDTree
        return cKDTree(self.face_adjacency_edges)

    @caching.cache_decorator
    def face_adjacency_angles(self):
        """
        Return the angle between adjacent faces

        Returns
        --------
        adjacency_angle : (n, ) float
          Angle between adjacent faces
          Each value corresponds with self.face_adjacency
        """
        # get pairs of unit vectors for adjacent faces
        pairs = self.face_normals[self.face_adjacency]
        # find the angle between the pairs of vectors
        angles = geometry.vector_angle(pairs)
        return angles

    @caching.cache_decorator
    def face_adjacency_projections(self):
        """
        The projection of the non-shared vertex of a triangle onto
        its adjacent face

        Returns
        ----------
        projections : (len(self.face_adjacency), ) float
          Dot product of vertex
          onto plane of adjacent triangle.
        """
        projections = convex.adjacency_projections(self)
        return projections

    @caching.cache_decorator
    def face_adjacency_convex(self):
        """
        Return faces which are adjacent and locally convex.

        What this means is that given faces A and B, the one vertex
        in B that is not shared with A, projected onto the plane of A
        has a projection that is zero or negative.

        Returns
        ----------
        are_convex : (len(self.face_adjacency), ) bool
          Face pairs that are locally convex
        """
        are_convex = self.face_adjacency_projections < tol.merge
        return are_convex

    @caching.cache_decorator
    def face_adjacency_unshared(self):
        """
        Return the vertex index of the two vertices not in the shared
        edge between two adjacent faces

        Returns
        -----------
        vid_unshared : (len(mesh.face_adjacency), 2) int
          Indexes of mesh.vertices
        """
        vid_unshared = graph.face_adjacency_unshared(self)
        return vid_unshared

    @caching.cache_decorator
    def face_adjacency_radius(self):
        """
        The approximate radius of a cylinder that fits inside adjacent faces.

        Returns
        ------------
        radii : (len(self.face_adjacency), ) float
          Approximate radius formed by triangle pair
        """
        radii, span = graph.face_adjacency_radius(mesh=self)
        self._cache['face_adjacency_span'] = span
        return radii

    @caching.cache_decorator
    def face_adjacency_span(self):
        """
        The approximate perpendicular projection of the non-shared
        vertices in a pair of adjacent faces onto the shared edge of
        the two faces.

        Returns
        ------------
        span : (len(self.face_adjacency), ) float
          Approximate span between the non-shared vertices
        """
        _ = self.face_adjacency_radius
        return self._cache['face_adjacency_span']

    @caching.cache_decorator
    def integral_mean_curvature(self):
        """
        The integral mean curvature, or the surface integral of the mean curvature.

        Returns
        ---------
        area : float
          Integral mean curvature of mesh
        """
        edges_length = np.linalg.norm(np.subtract(
            *self.vertices[self.face_adjacency_edges.T]), axis=1)
        imc = (self.face_adjacency_angles * edges_length).sum() * 0.5
        return imc

    @caching.cache_decorator
    def vertex_adjacency_graph(self):
        """
        Returns a networkx graph representing the vertices and their connections
        in the mesh.

        Returns
        ---------
        graph: networkx.Graph
          Graph representing vertices and edges between
          them where vertices are nodes and edges are edges

        Examples
        ----------
        This is useful for getting nearby vertices for a given vertex,
        potentially for some simple smoothing techniques.

        mesh = trimesh.primitives.Box()
        graph = mesh.vertex_adjacency_graph
        graph.neighbors(0)
        > [1, 2, 3, 4]
        """

        adjacency_g = graph.vertex_adjacency_graph(mesh=self)
        return adjacency_g

    @caching.cache_decorator
    def vertex_neighbors(self):
        """
        The vertex neighbors of each vertex of the mesh, determined from
        the cached vertex_adjacency_graph, if already existent.

        Returns
        ----------
        vertex_neighbors : (len(self.vertices), ) int
          Represents immediate neighbors of each vertex along
          the edge of a triangle

        Examples
        ----------
        This is useful for getting nearby vertices for a given vertex,
        potentially for some simple smoothing techniques.

        >>> mesh = trimesh.primitives.Box()
        >>> mesh.vertex_neighbors[0]
        [1, 2, 3, 4]
        """
        return graph.neighbors(
            edges=self.edges_unique,
            max_index=len(self.vertices))

    @caching.cache_decorator
    def is_winding_consistent(self):
        """
        Does the mesh have consistent winding or not.
        A mesh with consistent winding has each shared edge
        going in an opposite direction from the other in the pair.

        Returns
        --------
        consistent : bool
          Is winding is consistent or not
        """
        if self.is_empty:
            return False
        # consistent winding check is populated into the cache by is_watertight
        _ = self.is_watertight
        return self._cache['is_winding_consistent']

    @caching.cache_decorator
    def is_watertight(self):
        """
        Check if a mesh is watertight by making sure every edge is
        included in two faces.

        Returns
        ----------
        is_watertight : bool
          Is mesh watertight or not
        """
        if self.is_empty:
            return False
        watertight, winding = graph.is_watertight(
            edges=self.edges, edges_sorted=self.edges_sorted)
        self._cache['is_winding_consistent'] = winding
        return watertight

    @caching.cache_decorator
    def is_volume(self):
        """
        Check if a mesh has all the properties required to represent
        a valid volume, rather than just a surface.

        These properties include being watertight, having consistent
        winding and outward facing normals.

        Returns
        ---------
        valid : bool
          Does the mesh represent a volume
        """
        valid = bool(self.is_watertight and
                     self.is_winding_consistent and
                     np.isfinite(self.center_mass).all() and
                     self.volume > 0.0)
        return valid

    @property
    def is_empty(self):
        """
        Does the current mesh have data defined.

        Returns
        --------
        empty : bool
          If True, no data is set on the current mesh
        """
        return self._data.is_empty()

    @caching.cache_decorator
    def is_convex(self):
        """
        Check if a mesh is convex or not.

        Returns
        ----------
        is_convex: bool
          Is mesh convex or not
        """
        if self.is_empty:
            return False

        is_convex = bool(convex.is_convex(self))
        return is_convex

    @caching.cache_decorator
    def kdtree(self):
        """
        Return a scipy.spatial.cKDTree of the vertices of the mesh.
        Not cached as this lead to observed memory issues and segfaults.

        Returns
        ---------
        tree : scipy.spatial.cKDTree
          Contains mesh.vertices
        """

        from scipy.spatial import cKDTree
        tree = cKDTree(self.vertices.view(np.ndarray))
        return tree

    def remove_degenerate_faces(self, height=tol.merge):
        """
        DERECATED MARCH 2024 REPLACE WITH:
        `self.update_faces(self.nondegenerate_faces(height=height))`
        """
        warnings.warn(
            '`remove_degenerate_faces` is deprecated ' +
            'and will be removed in March 2024 replace with ' +
            '`self.update_faces(self.nondegenerate_faces(height=height))`',
            category=DeprecationWarning, stacklevel=2)
        self.update_faces(self.nondegenerate_faces(height=height))

    def nondegenerate_faces(self, height=tol.merge):
        """
        Remove degenerate faces (faces without 3 unique vertex indices)
        from the current mesh.

        If a height is specified, it will remove any face with a 2D oriented
        bounding box with one edge shorter than that height.

        If not specified, it will remove any face with a zero normal.

        Parameters
        ------------
        height : float
          If specified removes faces with an oriented bounding
          box shorter than this on one side.

        Returns
        -------------
        nondegenerate : (len(self.faces), ) bool
          Mask used to remove faces
        """
        return triangles.nondegenerate(
            self.triangles,
            areas=self.area_faces,
            height=height)

    @caching.cache_decorator
    def facets(self):
        """
        Return a list of face indices for coplanar adjacent faces.

        Returns
        ---------
        facets : (n, ) sequence of (m, ) int
          Groups of indexes of self.faces
        """
        facets = graph.facets(self)
        return facets

    @caching.cache_decorator
    def facets_area(self):
        """
        Return an array containing the area of each facet.

        Returns
        ---------
        area : (len(self.facets), ) float
          Total area of each facet (group of faces)
        """
        # avoid thrashing the cache inside a loop
        area_faces = self.area_faces
        # sum the area of each group of faces represented by facets
        # use native python sum in tight loop as opposed to array.sum()
        # as in this case the lower function call overhead of
        # native sum provides roughly a 50% speedup
        areas = np.array([sum(area_faces[i])
                          for i in self.facets],
                         dtype=np.float64)
        return areas

    @caching.cache_decorator
    def facets_normal(self):
        """
        Return the normal of each facet

        Returns
        ---------
        normals: (len(self.facets), 3) float
          A unit normal vector for each facet
        """
        if len(self.facets) == 0:
            return np.array([])

        area_faces = self.area_faces

        # the face index of the largest face in each facet
        index = np.array([i[area_faces[i].argmax()]
                          for i in self.facets])
        # (n, 3) float, unit normal vectors of facet plane
        normals = self.face_normals[index]
        # (n, 3) float, points on facet plane
        origins = self.vertices[self.faces[:, 0][index]]
        # save origins in cache
        self._cache['facets_origin'] = origins

        return normals

    @caching.cache_decorator
    def facets_origin(self):
        """
        Return a point on the facet plane.

        Returns
        ------------
        origins : (len(self.facets), 3) float
          A point on each facet plane
        """
        _ = self.facets_normal
        return self._cache['facets_origin']

    @caching.cache_decorator
    def facets_boundary(self):
        """
        Return the edges which represent the boundary of each facet

        Returns
        ---------
        edges_boundary : sequence of (n, 2) int
          Indices of self.vertices
        """
        # make each row correspond to a single face
        edges = self.edges_sorted.reshape((-1, 6))
        # get the edges for each facet
        edges_facet = [edges[i].reshape((-1, 2)) for i in self.facets]
        edges_boundary = [i[grouping.group_rows(i, require_count=1)]
                          for i in edges_facet]
        return edges_boundary

    @caching.cache_decorator
    def facets_on_hull(self):
        """
        Find which facets of the mesh are on the convex hull.

        Returns
        ---------
        on_hull : (len(mesh.facets), ) bool
          is A facet on the meshes convex hull or not
        """
        # if no facets exit early
        if len(self.facets) == 0:
            return np.array([], dtype=bool)

        # facets plane, origin and normal
        normals = self.facets_normal
        origins = self.facets_origin

        # (n, 3) convex hull vertices
        convex = self.convex_hull.vertices.view(np.ndarray).copy()

        # boolean mask for which facets are on convex hull
        on_hull = np.zeros(len(self.facets), dtype=bool)

        for i, normal, origin in zip(range(len(normals)), normals, origins):
            # a facet plane is on the convex hull if every vertex
            # of the convex hull is behind that plane
            # which we are checking with dot products
            dot = np.dot(normal, (convex - origin).T)
            on_hull[i] = (dot < tol.merge).all()

        return on_hull

    @log_time
    def fix_normals(self, multibody=None):
        """
        Find and fix problems with self.face_normals and self.faces
        winding direction.

        For face normals ensure that vectors are consistently pointed
        outwards, and that self.faces is wound in the correct direction
        for all connected components.

        Parameters
        -------------
        multibody : None or bool
          Fix normals across multiple bodies
          if None automatically pick from body_count
        """
        if multibody is None:
            multibody = self.body_count > 1
        repair.fix_normals(self, multibody=multibody)

    def fill_holes(self):
        """
        Fill single triangle and single quad holes in the current mesh.

        Returns
        ----------
        watertight : bool
          Is the mesh watertight after the function completes
        """
        return repair.fill_holes(self)

    def register(self, other, **kwargs):
        """
        Align a mesh with another mesh or a PointCloud using
        the principal axes of inertia as a starting point which
        is refined by iterative closest point.

        Parameters
        ------------
        mesh : trimesh.Trimesh object
          Mesh to align with other
        other : trimesh.Trimesh or (n, 3) float
          Mesh or points in space
        samples : int
          Number of samples from mesh surface to align
        icp_first : int
          How many ICP iterations for the 9 possible
          combinations of
        icp_final : int
          How many ICP itertations for the closest
          candidate from the wider search

        Returns
        -----------
        mesh_to_other : (4, 4) float
          Transform to align mesh to the other object
        cost : float
          Average square distance per point
        """
        mesh_to_other, cost = registration.mesh_other(
            mesh=self,
            other=other,
            **kwargs)
        return mesh_to_other, cost

    def compute_stable_poses(self,
                             center_mass=None,
                             sigma=0.0,
                             n_samples=1,
                             threshold=0.0):
        """
        Computes stable orientations of a mesh and their quasi-static probabilities.

        This method samples the location of the center of mass from a multivariate
        gaussian (mean at com, cov equal to identity times sigma) over n_samples.
        For each sample, it computes the stable resting poses of the mesh on a
        a planar workspace and evaluates the probabilities of landing in
        each pose if the object is dropped onto the table randomly.

        This method returns the 4x4 homogeneous transform matrices that place
        the shape against the planar surface with the z-axis pointing upwards
        and a list of the probabilities for each pose.
        The transforms and probabilties that are returned are sorted, with the
        most probable pose first.

        Parameters
        ------------
        center_mass : (3, ) float
          The object center of mass (if None, this method
          assumes uniform density and watertightness and
          computes a center of mass explicitly)
        sigma : float
          The covariance for the multivariate gaussian used
          to sample center of mass locations
        n_samples : int
          The number of samples of the center of mass location
        threshold : float
          The probability value at which to threshold
          returned stable poses

        Returns
        -------
        transforms : (n, 4, 4) float
          The homogeneous matrices that transform the
          object to rest in a stable pose, with the
          new z-axis pointing upwards from the table
          and the object just touching the table.

        probs : (n, ) float
          A probability ranging from 0.0 to 1.0 for each pose
        """
        return poses.compute_stable_poses(mesh=self,
                                          center_mass=center_mass,
                                          sigma=sigma,
                                          n_samples=n_samples,
                                          threshold=threshold)

    def subdivide(self, face_index=None):
        """
        Subdivide a mesh, with each subdivided face replaced with four
        smaller faces.

        Parameters
        ------------
        face_index: (m, ) int or None
          If None all faces of mesh will be subdivided
          If (m, ) int array of indices: only specified faces will be
          subdivided. Note that in this case the mesh will generally
          no longer be manifold, as the additional vertex on the midpoint
          will not be used by the adjacent faces to the faces specified,
          and an additional postprocessing step will be required to
          make resulting mesh watertight
        """
        # subdivide vertex attributes
        vertex_attributes = {}
        visual = None
        if (hasattr(self.visual, 'uv') and
                np.shape(self.visual.uv) == (len(self.vertices), 2)):

            # uv coords divided along with vertices
            vertices, faces, attr = remesh.subdivide(
                vertices=np.hstack((self.vertices, self.visual.uv)),
                faces=self.faces,
                face_index=face_index,
                vertex_attributes=vertex_attributes)

            # get a copy of the current visuals
            visual = self.visual.copy()

            # separate uv coords and vertices
            vertices, visual.uv = vertices[:, :3], vertices[:, 3:]

        else:
            # perform the subdivision with vertex attributes
            vertices, faces, attr = remesh.subdivide(
                vertices=self.vertices,
                faces=self.faces,
                face_index=face_index,
                vertex_attributes=vertex_attributes)

        # create a new mesh
        result = Trimesh(
            vertices=vertices,
            faces=faces,
            visual=visual,
            vertex_attributes=attr,
            process=False)
        return result

    def subdivide_to_size(self, max_edge, max_iter=10, return_index=False):
        """
        Subdivide a mesh until every edge is shorter than a
        specified length.

        Will return a triangle soup, not a nicely structured mesh.

        Parameters
        ------------
        max_edge : float
            Maximum length of any edge in the result
        max_iter : int
            The maximum number of times to run subdivision
        return_index : bool
            If True, return index of original face for new faces
        """
        # subdivide vertex attributes
        visual = None
        if (hasattr(self.visual, 'uv') and
                np.shape(self.visual.uv) == (len(self.vertices), 2)):

            # uv coords divided along with vertices
            vertices_faces = remesh.subdivide_to_size(
                vertices=np.hstack((self.vertices, self.visual.uv)),
                faces=self.faces,
                max_edge=max_edge,
                max_iter=max_iter,
                return_index=return_index)
            # unpack result
            if return_index:
                vertices, faces, final_index = vertices_faces
            else:
                vertices, faces = vertices_faces

            # get a copy of the current visuals
            visual = self.visual.copy()

            # separate uv coords and vertices
            vertices, visual.uv = vertices[:, :3], vertices[:, 3:]

        else:
            # uv coords divided along with vertices
            vertices_faces = remesh.subdivide_to_size(
                vertices=self.vertices,
                faces=self.faces,
                max_edge=max_edge,
                max_iter=max_iter,
                return_index=return_index)
            # unpack result
            if return_index:
                vertices, faces, final_index = vertices_faces
            else:
                vertices, faces = vertices_faces

        # create a new mesh
        result = Trimesh(
            vertices=vertices,
            faces=faces,
            visual=visual,
            process=False)

        if return_index:
            return result, final_index

        return result

    def subdivide_loop(self, iterations=None):
        """
        Subdivide a mesh by dividing each triangle into four
        triangles and approximating their smoothed surface
        using loop subdivision. Loop subdivision often looks
        better on triangular meshes than catmul-clark, which
        operates primarily on quads.

        Parameters
        ------------
        iterations : int
          Number of iterations to run subdivision.
        multibody : bool
          If True will try to subdivide for each submesh
        """
        # perform subdivision for one mesh
        new_vertices, new_faces = remesh.subdivide_loop(
            vertices=self.vertices,
            faces=self.faces,
            iterations=iterations)
        # create new mesh
        result = Trimesh(
            vertices=new_vertices,
            faces=new_faces,
            process=False)
        return result

    @log_time
    def smoothed(self, **kwargs):
        """
        Return a version of the current mesh which will render
        nicely, without changing source mesh.

        Parameters
        -------------
        angle : float or None
          Angle in radians face pairs with angles
          smaller than this will appear smoothed
        facet_minarea : float or None
          Minimum area fraction to consider
          IE for `facets_minarea=25` only facets larger
          than `mesh.area / 25` will be considered.

        Returns
        ---------
        smoothed : trimesh.Trimesh
          Non watertight version of current mesh
          which will render nicely with smooth shading
        """

        # smooth should be recomputed if visuals change
        self.visual._verify_hash()
        cached = self.visual._cache['smoothed']
        if cached is not None:
            return cached
        # run smoothing
        smoothed = graph.smoothed(
            self, **kwargs)
        self.visual._cache['smoothed'] = smoothed
        return smoothed

    @property
    def visual(self):
        """
        Get the stored visuals for the current mesh.

        Returns
        -------------
        visual : ColorVisuals or TextureVisuals
          Contains visual information about the mesh
        """
        if hasattr(self, '_visual'):
            return self._visual
        return None

    @visual.setter
    def visual(self, value):
        """
        When setting a visual object, always make sure
        that `visual.mesh` points back to the source mesh.

        Parameters
        --------------
        visual : ColorVisuals or TextureVisuals
          Contains visual information about the mesh
        """
        value.mesh = self
        self._visual = value

    def section(self,
                plane_normal,
                plane_origin,
                **kwargs):
        """
        Returns a 3D cross section of the current mesh and a plane
        defined by origin and normal.

        Parameters
        ------------
        plane_normal: (3) vector for plane normal
          Normal vector of section plane
        plane_origin : (3, ) float
          Point on the cross section plane

        Returns
        ---------
        intersections: Path3D or None
          Curve of intersection
        """
        # turn line segments into Path2D/Path3D objects
        from .exchange.load import load_path

        # return a single cross section in 3D
        lines, face_index = intersections.mesh_plane(
            mesh=self,
            plane_normal=plane_normal,
            plane_origin=plane_origin,
            return_faces=True,
            **kwargs)

        # if the section didn't hit the mesh return None
        if len(lines) == 0:
            return None

        # otherwise load the line segments into a Path3D object
        path = load_path(lines)

        # add the face index info into metadata
        path.metadata['face_index'] = face_index

        return path

    def section_multiplane(self,
                           plane_origin,
                           plane_normal,
                           heights):
        """
        Return multiple parallel cross sections of the current
        mesh in 2D.

        Parameters
        ------------
        plane_origin : (3, ) float
          Point on the cross section plane
        plane_normal: (3) vector for plane normal
          Normal vector of section plane
        heights : (n, ) float
          Each section is offset by height along
          the plane normal.

        Returns
        ---------
        paths : (n, ) Path2D or None
          2D cross sections at specified heights.
          path.metadata['to_3D'] contains transform
          to return 2D section back into 3D space.
        """
        # turn line segments into Path2D/Path3D objects
        from .exchange.load import load_path
        # do a multiplane intersection
        lines, transforms, faces = intersections.mesh_multiplane(
            mesh=self,
            plane_normal=plane_normal,
            plane_origin=plane_origin,
            heights=heights)

        # turn the line segments into Path2D objects
        paths = [None] * len(lines)
        for i, f, segments, T in zip(range(len(lines)),
                                     faces,
                                     lines,
                                     transforms):
            if len(segments) > 0:
                paths[i] = load_path(
                    segments,
                    metadata={'to_3D': T, 'face_index': f})
        return paths

    def slice_plane(self,
                    plane_origin,
                    plane_normal,
                    cap=False,
                    face_index=None,
                    cached_dots=None,
                    **kwargs):
        """
        Slice the mesh with a plane, returning a new mesh that is the
        portion of the original mesh to the positive normal side of the plane

        plane_origin :  (3,) float
          Point on plane to intersect with mesh
        plane_normal : (3,) float
          Normal vector of plane to intersect with mesh
        cap : bool
          If True, cap the result with a triangulated polygon
        face_index : ((m,) int)
            Indexes of mesh.faces to slice. When no mask is
            provided, the default is to slice all faces.
        cached_dots : (n, 3) float
            If an external function has stored dot
            products pass them here to avoid recomputing

        Returns
        ---------
        new_mesh: trimesh.Trimesh or None
          Subset of current mesh that intersects the half plane
          to the positive normal side of the plane
        """

        # return a new mesh
        new_mesh = intersections.slice_mesh_plane(
            mesh=self,
            plane_normal=plane_normal,
            plane_origin=plane_origin,
            cap=cap,
            face_index=face_index,
            cached_dots=cached_dots,
            **kwargs)

        return new_mesh

    def unwrap(self, image=None):
        """
        Returns a Trimesh object equivalent to the current mesh where
        the vertices have been assigned uv texture coordinates. Vertices
        may be split into as many as necessary by the unwrapping
        algorithm, depending on how many uv maps they appear in.

        Requires `pip install xatlas`

        Parameters
        ------------
        image : None or PIL.Image
          Image to assign to the material

        Returns
        --------
        unwrapped : trimesh.Trimesh
          Mesh with unwrapped uv coordinates
        """
        import xatlas

        vmap, faces, uv = xatlas.parametrize(
            self.vertices, self.faces)

        result = Trimesh(vertices=self.vertices[vmap],
                         faces=faces,
                         visual=TextureVisuals(uv=uv, image=image),
                         process=False)

        # run additional checks for unwrapping
        if tol.strict:
            # check the export object to make sure we didn't
            # move the indices around on creation
            assert np.allclose(result.visual.uv, uv)
            assert np.allclose(result.faces, faces)
            assert np.allclose(result.vertices, self.vertices[vmap])
            # check to make sure indices are still the
            # same order after we've exported to OBJ
            export = result.export(file_type='obj')
            uv_recon = np.array([L[3:].split() for L in
                                 str.splitlines(export) if
                                 L.startswith('vt ')],
                                dtype=np.float64)
            assert np.allclose(uv_recon, uv)
            v_recon = np.array([L[2:].split() for L in
                                str.splitlines(export) if
                                L.startswith('v ')],
                               dtype=np.float64)
            assert np.allclose(v_recon, self.vertices[vmap])

        return result

    @caching.cache_decorator
    def convex_hull(self):
        """
        Returns a Trimesh object representing the convex hull of
        the current mesh.

        Returns
        --------
        convex : trimesh.Trimesh
          Mesh of convex hull of current mesh
        """
        hull = convex.convex_hull(self)
        return hull

    def sample(self, count, return_index=False, face_weight=None):
        """
        Return random samples distributed across the
        surface of the mesh

        Parameters
        ------------
        count : int
          Number of points to sample
        return_index : bool
          If True will also return the index of which face each
          sample was taken from.
        face_weight : None or len(mesh.faces) float
          Weight faces by a factor other than face area.
          If None will be the same as face_weight=mesh.area

        Returns
        ---------
        samples : (count, 3) float
          Points on surface of mesh
        face_index : (count, ) int
          Index of self.faces
        """
        samples, index = sample.sample_surface(
            mesh=self, count=count, face_weight=face_weight)
        if return_index:
            return samples, index
        return samples

    def remove_unreferenced_vertices(self):
        """
        Remove all vertices in the current mesh which are not
        referenced by a face.
        """
        referenced = np.zeros(len(self.vertices), dtype=bool)
        referenced[self.faces] = True

        inverse = np.zeros(len(self.vertices), dtype=np.int64)
        inverse[referenced] = np.arange(referenced.sum())

        self.update_vertices(mask=referenced, inverse=inverse)

    def unmerge_vertices(self):
        """
        Removes all face references so that every face contains
        three unique vertex indices and no faces are adjacent.
        """
        # new faces are incrementing so every vertex is unique
        faces = np.arange(len(self.faces) * 3,
                          dtype=np.int64).reshape((-1, 3))

        # use update_vertices to apply mask to
        # all properties that are per-vertex
        self.update_vertices(self.faces.reshape(-1))
        # set faces to incrementing indexes
        self.faces = faces
        # keep face normals as the haven't changed
        self._cache.clear(exclude=['face_normals'])

    def apply_transform(self, matrix):
        """
        Transform mesh by a homogeneous transformation matrix.

        Does the bookkeeping to avoid recomputing things so this function
        should be used rather than directly modifying self.vertices
        if possible.

        Parameters
        ------------
        matrix : (4, 4) float
          Homogeneous transformation matrix
        """
        # get c-order float64 matrix
        matrix = np.asanyarray(
            matrix, order='C', dtype=np.float64)

        # only support homogeneous transformations
        if matrix.shape != (4, 4):
            raise ValueError('Transformation matrix must be (4, 4)!')

        # exit early if we've been passed an identity matrix
        # np.allclose is surprisingly slow so do this test
        elif util.allclose(matrix, np.eye(4), 1e-8):
            return self

        # new vertex positions
        new_vertices = transformations.transform_points(
            self.vertices,
            matrix=matrix)

        # check to see if the matrix has rotation
        # rather than just translation
        has_rotation = not util.allclose(
            matrix[:3, :3], np.eye(3), atol=1e-6)

        # overridden center of mass
        if self._center_mass is not None:
            self._center_mass = transformations.transform_points(
                np.array([self._center_mass, ]),
                matrix)[0]

        # preserve face normals if we have them stored
        if has_rotation and 'face_normals' in self._cache:
            # transform face normals by rotation component
            self._cache.cache['face_normals'] = util.unitize(
                transformations.transform_points(
                    self.face_normals,
                    matrix=matrix,
                    translate=False))

        # preserve vertex normals if we have them stored
        if has_rotation and 'vertex_normals' in self._cache:
            self._cache.cache['vertex_normals'] = util.unitize(
                transformations.transform_points(
                    self.vertex_normals,
                    matrix=matrix,
                    translate=False))

        # if transformation flips winding of triangles
        if has_rotation and transformations.flips_winding(matrix):
            log.debug('transform flips winding')
            # fliplr will make array non C contiguous
            # which will cause hashes to be more
            # expensive than necessary so wrap
            self.faces = np.ascontiguousarray(
                np.fliplr(self.faces))

        # assign the new values
        self.vertices = new_vertices

        # preserve normals and topology in cache
        # while dumping everything else
        self._cache.clear(exclude={
            'face_normals',   # transformed by us
            'vertex_normals',  # also transformed by us
            'face_adjacency',  # topological
            'face_adjacency_edges',
            'face_adjacency_unshared',
            'edges',
            'edges_face',
            'edges_sorted',
            'edges_unique',
            'edges_unique_idx',
            'edges_unique_inverse',
            'edges_sparse',
            'body_count',
            'faces_unique_edges',
            'euler_number'})
        # set the cache ID with the current hash value
        self._cache.id_set()
        return self

    def voxelized(self, pitch, method='subdivide', **kwargs):
        """
        Return a VoxelGrid object representing the current mesh
        discretized into voxels at the specified pitch

        Parameters
        ------------
        pitch : float
          The edge length of a single voxel
        method: implementation key. See `trimesh.voxel.creation.voxelizers`
        **kwargs: additional kwargs passed to the specified implementation.

        Returns
        ----------
        voxelized : VoxelGrid object
          Representing the current mesh
        """
        from .voxel import creation
        return creation.voxelize(
            mesh=self, pitch=pitch, method=method, **kwargs)

    @caching.cache_decorator
    def as_open3d(self):
        """
        Return an `open3d.geometry.TriangleMesh` version of
        the current mesh.

        Returns
        ---------
        open3d : open3d.geometry.TriangleMesh
          Current mesh as an open3d object.
        """
        import open3d
        # create from numpy arrays
        return open3d.geometry.TriangleMesh(
            vertices=open3d.utility.Vector3dVector(
                self.vertices.copy()),
            triangles=open3d.utility.Vector3iVector(
                self.faces.copy()))

    def simplify_quadratic_decimation(self, *args, **kwargs):
        """
        DERECATED MARCH 2024 REPLACE WITH:
        `mesh.simplify_quadric_decimation`
        """
        warnings.warn(
            '`simplify_quadratic_decimation` is deprecated ' +
            'as it was a typo and will be removed in March 2024: ' +
            'replace with `simplify_quadric_decimation`',
            category=DeprecationWarning, stacklevel=2)
        return self.simplify_quadric_decimation(*args, **kwargs)

    def simplify_quadric_decimation(self, face_count):
        """
        A thin wrapper around the `open3d` implementation of this:
        `open3d.geometry.TriangleMesh.simplify_quadric_decimation`

        Parameters
        -----------
        face_count : int
          Number of faces desired in the resulting mesh.

        Returns
        ---------
        simple : trimesh.Trimesh
          Simplified version of mesh.
        """
        simple = self.as_open3d.simplify_quadric_decimation(
            int(face_count))
        return Trimesh(vertices=simple.vertices, faces=simple.triangles)

    def outline(self, face_ids=None, **kwargs):
        """
        Given a list of face indexes find the outline of those
        faces and return it as a Path3D.

        The outline is defined here as every edge which is only
        included by a single triangle.

        Note that this implies a non-watertight mesh as the
        outline of a watertight mesh is an empty path.

        Parameters
        ------------
        face_ids : (n, ) int
          Indices to compute the outline of.
          If None, outline of full mesh will be computed.
        **kwargs: passed to Path3D constructor

        Returns
        ----------
        path : Path3D
          Curve in 3D of the outline
        """
        from .path import Path3D
        from .path.exchange.misc import faces_to_path
        return Path3D(**faces_to_path(
            self, face_ids, **kwargs))

    def projected(self,
                  normal,
                  **kwargs):
        """
        Project a mesh onto a plane and then extract the
        polygon that outlines the mesh projection on that
        plane.

        Parameters
        ----------
        mesh : trimesh.Trimesh
          Source geometry
        check : bool
          If True make sure is flat
        normal : (3,) float
          Normal to extract flat pattern along
        origin : None or (3,) float
          Origin of plane to project mesh onto
        pad : float
          Proportion to pad polygons by before unioning
          and then de-padding result by to avoid zero-width gaps.
        tol_dot : float
          Tolerance for discarding on-edge triangles.
        max_regions : int
          Raise an exception if the mesh has more than this
          number of disconnected regions to fail quickly before unioning.

        Returns
        ----------
        projected : trimesh.path.Path2D
          Outline of source mesh
        """
        from .path import Path2D
        from .exchange.load import load_path
        from .path.polygons import projected

        projection = projected(
            mesh=self, normal=normal, **kwargs)
        if projection is None:
            return Path2D()
        return load_path(projection)

    @caching.cache_decorator
    def area(self):
        """
        Summed area of all triangles in the current mesh.

        Returns
        ---------
        area : float
          Surface area of mesh
        """
        area = self.area_faces.sum()
        return area

    @caching.cache_decorator
    def area_faces(self):
        """
        The area of each face in the mesh.

        Returns
        ---------
        area_faces : (n, ) float
          Area of each face
        """
        area_faces = triangles.area(
            crosses=self.triangles_cross,
            sum=False)
        return area_faces

    @caching.cache_decorator
    def mass_properties(self):
        """
        Returns the mass properties of the current mesh.

        Assumes uniform density, and result is probably garbage if mesh
        isn't watertight.

        Returns
        ----------
        properties : dict
          With keys:
          'volume'      : in global units^3
          'mass'        : From specified density
          'density'     : Included again for convenience (same as kwarg density)
          'inertia'     : Taken at the center of mass and aligned with global
                         coordinate system
          'center_mass' : Center of mass location, in global coordinate system
        """
        mass = triangles.mass_properties(
            triangles=self.triangles,
            crosses=self.triangles_cross,
            density=self._density,
            center_mass=self._center_mass,
            skip_inertia=False)
        return mass

    def invert(self):
        """
        Invert the mesh in-place by reversing the winding of every
        face and negating normals without dumping the cache.

        Alters `self.faces` by reversing columns, and negating
        `self.face_normals` and `self.vertex_normals`.
        """
        with self._cache:
            if 'face_normals' in self._cache:
                self.face_normals = self._cache['face_normals'] * -1.0
            if 'vertex_normals' in self._cache:
                self.vertex_normals = self._cache['vertex_normals'] * -1.0
            # fliplr makes array non-contiguous so cache checks slow
            self.faces = np.ascontiguousarray(
                np.fliplr(self.faces))
        # save our normals
        self._cache.clear(exclude=['face_normals',
                                   'vertex_normals'])

    def scene(self, **kwargs):
        """
        Returns a Scene object containing the current mesh.

        Returns
        ---------
        scene : trimesh.scene.scene.Scene
          Contains just the current mesh
        """
        return Scene(self, **kwargs)

    def show(self, **kwargs):
        """
        Render the mesh in an opengl window. Requires pyglet.

        Parameters
        ------------
        smooth : bool
          Run smooth shading on mesh or not,
          large meshes will be slow

        Returns
        -----------
        scene : trimesh.scene.Scene
          Scene with current mesh in it
        """
        scene = self.scene()
        return scene.show(**kwargs)

    def submesh(self, faces_sequence, **kwargs):
        """
        Return a subset of the mesh.

        Parameters
        ------------
        faces_sequence : sequence (m, ) int
          Face indices of mesh
        only_watertight : bool
          Only return submeshes which are watertight
        append : bool
          Return a single mesh which has the faces appended.
          if this flag is set, only_watertight is ignored

        Returns
        ---------
        submesh : Trimesh or (n,) Trimesh
          Single mesh if `append` or list of submeshes
        """
        return util.submesh(
            mesh=self,
            faces_sequence=faces_sequence,
            **kwargs)

    @caching.cache_decorator
    def identifier(self):
        """
        Return a float vector which is unique to the mesh
        and is robust to rotation and translation.

        Returns
        -----------
        identifier : (7,) float
          Identifying properties of the current mesh
        """
        return comparison.identifier_simple(self)

    @caching.cache_decorator
    def identifier_hash(self):
        """
        A hash of the rotation invariant identifier vector.

        Returns
        ---------
        hashed : str
          Hex string of the SHA256 hash from
          the identifier vector at hand-tuned sigfigs.
        """
        return comparison.identifier_hash(self.identifier)

    @property
    def identifier_md5(self):
        warnings.warn(
            '`geom.identifier_md5` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geom.identifier_hash`',
            category=DeprecationWarning, stacklevel=2)
        return self.identifier_hash

    def export(self, file_obj=None, file_type=None, **kwargs):
        """
        Export the current mesh to a file object.
        If file_obj is a filename, file will be written there.

        Supported formats are stl, off, ply, collada, json,
        dict, glb, dict64, msgpack.

        Parameters
        ------------
        file_obj : open writeable file object
          str, file name where to save the mesh
          None, return the export blob
        file_type : str
          Which file type to export as, if `file_name`
          is passed this is not required.
        """
        return export_mesh(
            mesh=self,
            file_obj=file_obj,
            file_type=file_type,
            **kwargs)

    def to_dict(self):
        """
        Return a dictionary representation of the current mesh
        with keys that can be used as the kwargs for the
        Trimesh constructor and matches the schema in:
        `trimesh/resources/schema/primitive/trimesh.schema.json`

        Returns
        ----------
        result : dict
          Matches schema and Trimesh constructor.
        """
        return {'kind': 'trimesh',
                'vertices': self.vertices.tolist(),
                'faces': self.faces.tolist()}

    def convex_decomposition(self, maxhulls=20, **kwargs):
        """
        Compute an approximate convex decomposition of a mesh.

        testVHACD Parameters which can be passed as kwargs:

        Name                                        Default
        -----------------------------------------------------
        resolution                                  100000
        max. concavity                              0.001
        plane down-sampling                         4
        convex-hull down-sampling                   4
        alpha                                       0.05
        beta                                        0.05
        maxhulls                                    10
        pca                                         0
        mode                                        0
        max. vertices per convex-hull               64
        min. volume to add vertices to convex-hulls 0.0001
        convex-hull approximation                   1
        OpenCL acceleration                         1
        OpenCL platform ID                          0
        OpenCL device ID                            0
        output                                      output.wrl
        log                                         log.txt


        Parameters
        ------------
        maxhulls :  int
          Maximum number of convex hulls to return
        **kwargs :  testVHACD keyword arguments

        Returns
        -------
        meshes : list of trimesh.Trimesh
          List of convex meshes that approximate the original
        """
        result = decomposition.convex_decomposition(self,
                                                    maxhulls=maxhulls,
                                                    **kwargs)
        return result

    def union(self, other, engine=None, **kwargs):
        """
        Boolean union between this mesh and n other meshes

        Parameters
        ------------
        other : Trimesh or (n, ) Trimesh
          Other meshes to union
        engine : None or str
          Which backend to use

        Returns
        ---------
        union : trimesh.Trimesh
          Union of self and other Trimesh objects
        """
        result = boolean.union(
            meshes=np.append(self, other),
            engine=engine,
            **kwargs)
        return result

    def difference(self, other, engine=None, **kwargs):
        """
        Boolean difference between this mesh and n other meshes

        Parameters
        ------------
        other : trimesh.Trimesh, or list of trimesh.Trimesh objects
         Meshes to difference

        Returns
        ---------
        difference : trimesh.Trimesh
          Difference between self and other Trimesh objects
        """
        result = boolean.difference(meshes=np.append(self, other),
                                    engine=engine, **kwargs)
        return result

    def intersection(self, other, engine=None, **kwargs):
        """
        Boolean intersection between this mesh and n other meshes

        Parameters
        ------------
        other : trimesh.Trimesh, or list of trimesh.Trimesh objects
          Meshes to calculate intersections with

        Returns
        ---------
        intersection : trimesh.Trimesh
          Mesh of the volume contained by all passed meshes
        """
        result = boolean.intersection(meshes=np.append(self, other),
                                      engine=engine, **kwargs)
        return result

    def contains(self, points):
        """
        Given an array of points determine whether or not they
        are inside the mesh. This raises an error if called on a
        non-watertight mesh.

        Parameters
        ------------
        points : (n, 3) float
          Points in cartesian space

        Returns
        ---------
        contains : (n, ) bool
          Whether or not each point is inside the mesh
        """
        return self.ray.contains_points(points)

    @caching.cache_decorator
    def face_angles(self):
        """
        Returns the angle at each vertex of a face.

        Returns
        --------
        angles : (len(self.faces), 3) float
          Angle at each vertex of a face
        """
        angles = triangles.angles(self.triangles)
        return angles

    @caching.cache_decorator
    def face_angles_sparse(self):
        """
        A sparse matrix representation of the face angles.

        Returns
        ----------
        sparse : scipy.sparse.coo_matrix
          Float sparse matrix with with shape:
          (len(self.vertices), len(self.faces))
        """
        angles = curvature.face_angles_sparse(self)
        return angles

    @caching.cache_decorator
    def vertex_defects(self):
        """
        Return the vertex defects, or (2*pi) minus the sum of the angles
        of every face that includes that vertex.

        If a vertex is only included by coplanar triangles, this
        will be zero. For convex regions this is positive, and
        concave negative.

        Returns
        --------
        vertex_defect : (len(self.vertices), ) float
          Vertex defect at the every vertex
        """
        defects = curvature.vertex_defects(self)
        return defects

    @caching.cache_decorator
    def vertex_degree(self):
        """
        Return the number of faces each vertex is included in.

        Returns
        ----------
        degree : (len(self.vertices), ) int
          Number of faces each vertex is included in
        """
        # get degree through sparse matrix
        degree = np.array(self.faces_sparse.sum(axis=1)).flatten()
        return degree

    @caching.cache_decorator
    def face_adjacency_tree(self):
        """
        An R-tree of face adjacencies.

        Returns
        --------
        tree: rtree.index
          Where each edge in self.face_adjacency has a
          rectangular cell
        """
        # the (n,6) interleaved bounding box for every line segment
        segment_bounds = np.column_stack((
            self.vertices[self.face_adjacency_edges].min(axis=1),
            self.vertices[self.face_adjacency_edges].max(axis=1)))
        tree = util.bounds_tree(segment_bounds)
        return tree

    def copy(self, include_cache=False):
        """
        Safely return a copy of the current mesh.

        By default, copied meshes will have emptied cache
        to avoid memory issues and so may be slow on initial
        operations until caches are regenerated.

        Current object will *never* have its cache cleared.

        Parameters
        ------------
        include_cache : bool
          If True, will shallow copy cached data to new mesh

        Returns
        ---------
        copied : trimesh.Trimesh
          Copy of current mesh
        """
        # start with an empty mesh
        copied = Trimesh()
        # always deepcopy vertex and face data
        copied._data.data = copy.deepcopy(self._data.data)
        # copy visual information
        copied.visual = self.visual.copy()
        # get metadata
        copied.metadata = copy.deepcopy(self.metadata)
        # get center_mass and density
        if self._center_mass is not None:
            copied.center_mass = self.center_mass
        copied._density = self._density

        # make sure cache ID is set initially
        copied._cache.verify()

        if include_cache:
            # shallow copy cached items into the new cache
            # since the data didn't change here when the
            # data in the new mesh is changed these items
            # will be dumped in the new mesh but preserved
            # in the original mesh
            copied._cache.cache.update(self._cache.cache)

        return copied

    def __deepcopy__(self, *args):
        # interpret deep copy as "get rid of cached data"
        return self.copy(include_cache=False)

    def __copy__(self, *args):
        # interpret shallow copy as "keep cached data"
        return self.copy(include_cache=True)

    def eval_cached(self, statement, *args):
        """
        Evaluate a statement and cache the result before returning.

        Statements are evaluated inside the Trimesh object, and

        Parameters
        ------------
        statement : str
          Statement of valid python code
        *args : list
          Available inside statement as args[0], etc

        Returns
        -----------
        result : result of running eval on statement with args

        Examples
        -----------
        r = mesh.eval_cached('np.dot(self.vertices, args[0])', [0, 0, 1])
        """

        statement = str(statement)
        key = 'eval_cached_' + statement
        key += '_'.join(str(i) for i in args)

        if key in self._cache:
            return self._cache[key]

        result = eval(statement)
        self._cache[key] = result
        return result

    def __add__(self, other):
        """
        Concatenate the mesh with another mesh.

        Parameters
        ------------
        other : trimesh.Trimesh object
          Mesh to be concatenated with self

        Returns
        ----------
        concat : trimesh.Trimesh
          Mesh object of combined result
        """
        concat = util.concatenate(self, other)
        return concat


# trimesh/boolean.py

"""
boolean.py
-------------

Do boolean operations on meshes using either Blender or OpenSCAD.
"""
from . import interfaces


def difference(meshes, engine=None, **kwargs):
    """
    Compute the boolean difference between a mesh an n other meshes.

    Parameters
    ----------
    meshes : list of trimesh.Trimesh
      Meshes to be processed
    engine : str
      Which backend to use, i.e. 'blender' or 'scad'

    Returns
    ----------
    difference : a - (other meshes), **kwargs for a Trimesh
    """
    result = _engines[engine](meshes, operation='difference', **kwargs)
    return result


def union(meshes, engine=None, **kwargs):
    """
    Compute the boolean union between a mesh an n other meshes.

    Parameters
    ----------
    meshes : list of trimesh.Trimesh
      Meshes to be processed
    engine : str
      Which backend to use, i.e. 'blender' or 'scad'

    Returns
    ----------
    union : a + (other meshes), **kwargs for a Trimesh
    """
    result = _engines[engine](meshes, operation='union', **kwargs)
    return result


def intersection(meshes, engine=None, **kwargs):
    """
    Compute the boolean intersection between a mesh an n other meshes.

    Parameters
    ----------
    meshes : list of trimesh.Trimesh
      Meshes to be processed
    engine : str
      Which backend to use, i.e. 'blender' or 'scad'

    Returns
    ----------
    intersection : **kwargs for a Trimesh object of the
                    volume that is contained by all meshes
    """
    result = _engines[engine](meshes, operation='intersection', **kwargs)
    return result


def boolean_automatic(meshes, operation, **kwargs):
    """
    Automatically pick an engine for booleans based on availability.

    Parameters
    --------------
    meshes : list of Trimesh
      Meshes to be booleaned
    operation : str
      Type of boolean, i.e. 'union', 'intersection', 'difference'

    Returns
    ---------------
    result : trimesh.Trimesh
      Result of boolean operation
    """
    if interfaces.blender.exists:
        result = interfaces.blender.boolean(meshes, operation, **kwargs)
    elif interfaces.scad.exists:
        result = interfaces.scad.boolean(meshes, operation, **kwargs)
    else:
        raise ValueError('No backends available for boolean operations!')
    return result


# which backend boolean engines
_engines = {None: boolean_automatic,
            'auto': boolean_automatic,
            'scad': interfaces.scad.boolean,
            'blender': interfaces.blender.boolean}


# trimesh/bounds.py

import numpy as np
from .constants import log, now

from . import util
from . import convex
from . import nsphere
from . import geometry
from . import grouping
from . import transformations

try:
    # scipy is a soft dependency
    from scipy.spatial import ConvexHull
    from scipy import optimize
except BaseException as E:
    # raise the exception when someone tries to use it
    from . import exceptions
    ConvexHull = exceptions.ExceptionWrapper(E)
    optimize = exceptions.ExceptionWrapper(E)

try:
    from scipy.spatial import QhullError
except BaseException:
    QhullError = BaseException

# a 90 degree rotation
_flip = transformations.planar_matrix(theta=np.pi / 2)
_flip.flags.writeable = False


def oriented_bounds_2D(points, qhull_options='QbB'):
    """
    Find an oriented bounding box for an array of 2D points.

    Parameters
    ----------
    points : (n,2) float
      Points in 2D.

    Returns
    ----------
    transform : (3,3) float
      Homogeneous 2D transformation matrix to move the
      input points so that the axis aligned bounding box
      is CENTERED AT THE ORIGIN.
    rectangle : (2,) float
       Size of extents once input points are transformed
       by transform
    """
    # create a convex hull object of our points
    # 'QbB' is a qhull option which has it scale the input to unit
    # box to avoid precision issues with very large/small meshes
    convex = ConvexHull(
        points, qhull_options=qhull_options)

    # (n,2,3) line segments
    hull_edges = convex.points[convex.simplices]
    # (n,2) points on the convex hull
    hull_points = convex.points[convex.vertices]

    # unit vector direction of the edges of the hull polygon
    # filter out zero- magnitude edges via check_valid
    edge_vectors = hull_edges[:, 1] - hull_edges[:, 0]
    edge_norm = np.sqrt(np.dot(edge_vectors ** 2, [1, 1]))
    edge_nonzero = edge_norm > 1e-10
    edge_vectors = edge_vectors[edge_nonzero] / \
        edge_norm[edge_nonzero].reshape((-1, 1))

    # create a set of perpendicular vectors
    perp_vectors = np.fliplr(edge_vectors) * [-1.0, 1.0]

    # find the projection of every hull point on every edge vector
    # this does create a potentially gigantic n^2 array in memory,
    # and there is the 'rotating calipers' algorithm which avoids this
    # however, we have reduced n with a convex hull and numpy dot products
    # are extremely fast so in practice this usually ends up being fine
    x = np.dot(edge_vectors, hull_points.T)
    y = np.dot(perp_vectors, hull_points.T)

    # reduce the projections to maximum and minimum per edge vector
    bounds = np.column_stack((x.min(axis=1),
                              y.min(axis=1),
                              x.max(axis=1),
                              y.max(axis=1)))

    # calculate the extents and area for each edge vector pair
    extents = np.diff(bounds.reshape((-1, 2, 2)),
                      axis=1).reshape((-1, 2))
    area = np.prod(extents, axis=1)
    area_min = area.argmin()

    # (2,) float of smallest rectangle size
    rectangle = extents[area_min]

    # find the (3,3) homogeneous transformation which moves the input
    # points to have a bounding box centered at the origin
    offset = -bounds[area_min][:2] - (rectangle * .5)
    theta = np.arctan2(*edge_vectors[area_min][::-1])
    transform = transformations.planar_matrix(offset, theta)

    # we would like to consistently return an OBB with
    # the largest dimension along the X axis rather than
    # the long axis being arbitrarily X or Y.
    if rectangle[1] > rectangle[0]:
        # apply the rotation
        transform = np.dot(_flip, transform)
        # switch X and Y in the OBB extents
        rectangle = rectangle[::-1]

    return transform, rectangle


def oriented_bounds(obj,
                    angle_digits=1,
                    ordered=True,
                    normal=None,
                    coplanar_tol=1e-12):
    """
    Find the oriented bounding box for a Trimesh

    Parameters
    ----------
    obj : trimesh.Trimesh, (n, 2) float, or (n, 3) float
       Mesh object or points in 2D or 3D space
    angle_digits : int
       How much angular precision do we want on our result.
       Even with less precision the returned extents will cover
       the mesh albeit with larger than minimal volume, and may
       experience substantial speedups.
    ordered : bool
      Return a consistent order for bounds
    normal : None or (3,) float
      Override search for normal on 3D meshes.
    coplanar_tol : float
      If a convex hull fails and we are checking to see if the
      points are coplanar this is the maximum deviation from
      a plane where the points will be considered coplanar.

    Returns
    ----------
    to_origin : (4,4) float
      Transformation matrix which will move the center of the
      bounding box of the input mesh to the origin.
    extents: (3,) float
      The extents of the mesh once transformed with to_origin
    """

    def oriented_bounds_coplanar(points):
        """
        Find an oriented bounding box for an array of coplanar 3D points.

        Parameters
        ----------
        points : (n, 3) float
          Points in 3D that occupy a 2D subspace.

        Returns
        ----------
        to_origin : (4, 4) float
          Transformation matrix which will move the center of the
          bounding box of the input mesh to the origin.
        extents : (3,) float
          The extents of the mesh once transformed with to_origin
        """
        # Shift points about the origin and rotate into the xy plane
        points_mean = np.mean(points, axis=0)
        points_demeaned = points - points_mean
        _, _, vh = np.linalg.svd(points_demeaned, full_matrices=False)
        points_2d = np.matmul(points_demeaned, vh.T)
        if np.any(np.abs(points_2d[:, 2]) > coplanar_tol):
            raise ValueError('Points must be coplanar')

        # Construct a homogeneous matrix representing the transformation above
        to_2d = np.eye(4)
        to_2d[:3, :3] = vh
        to_2d[:3, 3] = -np.matmul(vh, points_mean)

        # Find the 2D bounding box using the polygon
        to_origin_2d, extents_2d = oriented_bounds_2D(points_2d[:, :2])
        # Make extents 3D
        extents = np.append(extents_2d, 0.0)
        # convert transformation from 2D to 3D and combine
        to_origin = np.matmul(
            transformations.planar_matrix_to_3D(to_origin_2d), to_2d)
        return to_origin, extents

    try:
        # extract a set of convex hull vertices and normals from the input
        # we bother to do this to avoid recomputing the full convex hull if
        # possible
        if hasattr(obj, 'convex_hull'):
            # if we have been passed a mesh, use its existing convex hull to pull from
            # cache rather than recomputing. This version of the cached convex hull has
            # normals pointing in arbitrary directions (straight from qhull)
            # using this avoids having to compute the expensive corrected normals
            # that mesh.convex_hull uses since normal directions don't matter
            # here
            hull = obj.convex_hull
        elif util.is_sequence(obj):
            # we've been passed a list of points
            points = np.asanyarray(obj)
            if util.is_shape(points, (-1, 2)):
                return oriented_bounds_2D(points)
            elif util.is_shape(points, (-1, 3)):
                hull = convex.convex_hull(points, repair=False)
            else:
                raise ValueError('Points are not (n,3) or (n,2)!')
        else:
            raise ValueError(
                'Oriented bounds must be passed a mesh or a set of points!')
    except QhullError:
        # Try to recover from Qhull error if due to mesh being less than 3
        # dimensional
        if hasattr(obj, 'vertices'):
            points = obj.vertices.view(np.ndarray)
        elif util.is_sequence(obj):
            points = np.asanyarray(obj)
        else:
            raise
        return oriented_bounds_coplanar(points)

    vertices = hull.vertices
    hull_adj = hull.face_adjacency.T
    hull_edge = hull.face_adjacency_edges
    hull_normals = hull.face_normals

    # matrices which will rotate each hull normal to [0,0,1]
    if normal is None:
        # convert face normals to spherical coordinates on the upper hemisphere
        # the vector_hemisphere call effectivly merges negative but otherwise
        # identical vectors
        spherical_coords = util.vector_to_spherical(
            util.vector_hemisphere(hull_normals))
        # the unique_rows call on merge angles gets unique spherical directions to check
        # we get a substantial speedup in the transformation matrix creation
        # inside the loop by converting to angles ahead of time
        spherical_unique = grouping.unique_rows(
            spherical_coords, digits=angle_digits)[0]
        matrices = [transformations.spherical_matrix(*s).T
                    for s in spherical_coords[spherical_unique]]
        normals = util.spherical_to_vector(spherical_coords[spherical_unique])
    else:
        # if explicit normal was passed use it and skip the grouping
        matrices = [geometry.align_vectors(normal, [0, 0, 1])]
        normals = [normal]

    tic = now()
    min_2D = None
    min_volume = np.inf

    # we now need to loop through all the possible candidate
    # directions for aligning our oriented bounding box.
    for normal, to_2D in zip(normals, matrices):
        # we could compute the hull in 2D for every direction
        # but since we know we're dealing with a convex blob
        # we can do back-face culling and then take the boundary
        # start by picking the normal direction with fewer edges
        side = np.dot(hull_normals, normal) > -1e-10
        # for coplanar points this could be empty
        if not side.any():
            continue
        # this line is a heavy lift as it is finding the pairs of
        # adjacent faces where *exactly one* out of two of the faces
        # is visible (xor) and then using the index to get the edge
        edges = hull_edge[np.bitwise_xor(*side[hull_adj])]

        # project the 3D convex hull vertices onto the plane
        projected = np.dot(to_2D[:3, :3], vertices.T).T[:, :3]
        # get the line segments of edges in 2D
        edge_vert = projected[:, :2][edges]
        # now get them as unit vectors
        edge_vectors = edge_vert[:, 1, :] - edge_vert[:, 0, :]
        edge_norm = np.sqrt(np.dot(edge_vectors ** 2, [1, 1]))
        edge_nonzero = edge_norm > 1e-10
        edge_vectors = edge_vectors[edge_nonzero] / \
            edge_norm[edge_nonzero].reshape((-1, 1))
        # create a set of perpendicular vectors
        perp_vectors = np.fliplr(edge_vectors) * [-1.0, 1.0]

        # find the projection of every hull point on every edge vector
        # this does create a potentially gigantic n^2 array in memory
        # and there is the 'rotating calipers' algorithm which avoids this
        # however, we have reduced n with a convex hull and numpy dot products
        # are extremely fast so in practice this usually ends up being fine
        x = np.dot(edge_vectors, edge_vert[:, 0, :2].T)
        y = np.dot(perp_vectors, edge_vert[:, 0, :2].T)
        area = ((x.max(axis=1) - x.min(axis=1)) *
                (y.max(axis=1) - y.min(axis=1))).min()

        # the volume is 2D area plus the projected height
        volume = area * projected[:, 2].ptp()

        # store this transform if it's better than one we've seen
        if volume < min_volume:
            min_volume = volume
            min_2D = to_2D

    # we know the minimum volume transform which should be the expensive
    # part so now we need to do the bookkeeping to find the box
    vert_ones = np.column_stack((vertices, np.ones(len(vertices)))).T
    projected = np.dot(min_2D, vert_ones).T[:, :3]
    height = projected[:, 2].ptp()
    rotation_2D, box = oriented_bounds_2D(projected[:, :2])
    min_extents = np.append(box, height)
    rotation_2D[:2, 2] = 0.0
    rotation_Z = transformations.planar_matrix_to_3D(rotation_2D)

    # combine the 2D OBB transformation with the 2D projection transform
    to_origin = np.dot(rotation_Z, min_2D)

    # transform points using our matrix to find the translation
    transformed = transformations.transform_points(vertices, to_origin)
    box_center = (transformed.min(axis=0) + transformed.ptp(axis=0) * .5)
    to_origin[:3, 3] = -box_center

    # return ordered 3D extents
    if ordered:
        # sort the three extents
        order = min_extents.argsort()
        # generate a matrix which will flip transform
        # to match the new ordering
        flip = np.eye(4)
        flip[:3, :3] = -np.eye(3)[order]

        # make sure transform isn't mangling triangles
        # by reversing windings on triangles
        if not np.isclose(np.linalg.det(flip[:3, :3]), 1.0):
            flip[:3, :3] = np.dot(flip[:3, :3], -np.eye(3))

        # apply the flip to the OBB transform
        to_origin = np.dot(flip, to_origin)
        # apply the order to the extents
        min_extents = min_extents[order]

    log.debug('oriented_bounds checked %d vectors in %0.4fs',
              len(matrices), now() - tic)

    return to_origin, min_extents


def minimum_cylinder(obj, sample_count=6, angle_tol=0.001):
    """
    Find the approximate minimum volume cylinder which contains
    a mesh or a a list of points.

    Samples a hemisphere then uses scipy.optimize to pick the
    final orientation of the cylinder.

    A nice discussion about better ways to implement this is here:
    https://www.staff.uni-mainz.de/schoemer/publications/ALGO00.pdf


    Parameters
    ----------
    obj : trimesh.Trimesh, or (n, 3) float
      Mesh object or points in space
    sample_count : int
      How densely should we sample the hemisphere.
      Angular spacing is 180 degrees / this number

    Returns
    ----------
    result : dict
      With keys:
        'radius'    : float, radius of cylinder
        'height'    : float, height of cylinder
        'transform' : (4,4) float, transform from the origin
                      to centered cylinder
    """

    def volume_from_angles(spherical, return_data=False):
        """
        Takes spherical coordinates and calculates the volume
        of a cylinder along that vector

        Parameters
        ---------
        spherical : (2,) float
           Theta and phi
        return_data : bool
           Flag for returned

        Returns
        --------
        if return_data:
            transform ((4,4) float)
            radius (float)
            height (float)
        else:
            volume (float)
        """
        to_2D = transformations.spherical_matrix(
            *spherical, axes='rxyz')
        projected = transformations.transform_points(
            hull, matrix=to_2D)
        height = projected[:, 2].ptp()

        try:
            center_2D, radius = nsphere.minimum_nsphere(projected[:, :2])
        except ValueError:
            return np.inf

        volume = np.pi * height * (radius ** 2)
        if return_data:
            center_3D = np.append(
                center_2D, projected[:, 2].min() + (height * .5))
            transform = np.dot(
                np.linalg.inv(to_2D),
                transformations.translation_matrix(center_3D))
            return transform, radius, height
        return volume

    # we've been passed a mesh with radial symmetry
    # use center mass and symmetry axis and go home early
    if hasattr(obj, 'symmetry') and obj.symmetry == 'radial':
        # find our origin
        if obj.is_watertight:
            # set origin to center of mass
            origin = obj.center_mass
        else:
            # convex hull should be watertight
            origin = obj.convex_hull.center_mass
        # will align symmetry axis with Z and move origin to zero
        to_2D = geometry.plane_transform(
            origin=origin,
            normal=obj.symmetry_axis)
        # transform vertices to plane to check
        on_plane = transformations.transform_points(
            obj.vertices, to_2D)
        # cylinder height is overall Z span
        height = on_plane[:, 2].ptp()
        # center mass is correct on plane, but position
        # along symmetry axis may be wrong so slide it
        slide = transformations.translation_matrix(
            [0, 0, (height / 2.0) - on_plane[:, 2].max()])
        to_2D = np.dot(slide, to_2D)
        # radius is maximum radius
        radius = (on_plane[:, :2] ** 2).sum(axis=1).max() ** 0.5
        # save kwargs
        result = {'height': height,
                  'radius': radius,
                  'transform': np.linalg.inv(to_2D)}
        return result

    # get the points on the convex hull of the result
    hull = convex.hull_points(obj)
    if not util.is_shape(hull, (-1, 3)):
        raise ValueError('Input must be reducable to 3D points!')

    # sample a hemisphere so local hill climbing can do its thing
    samples = util.grid_linspace([[0, 0], [np.pi, np.pi]], sample_count)

    # if it's rotationally symmetric the bounding cylinder
    # is almost certainly along one of the PCI vectors
    if hasattr(obj, 'principal_inertia_vectors'):
        # add the principal inertia vectors if we have a mesh
        samples = np.vstack(
            (samples,
             util.vector_to_spherical(obj.principal_inertia_vectors)))

    tic = [now()]
    # the projected volume at each sample
    volumes = np.array([volume_from_angles(i) for i in samples])
    # the best vector in (2,) spherical coordinates
    best = samples[volumes.argmin()]
    tic.append(now())

    # since we already explored the global space, set the bounds to be
    # just around the sample that had the lowest volume
    step = 2 * np.pi / sample_count
    bounds = [(best[0] - step, best[0] + step),
              (best[1] - step, best[1] + step)]
    # run the local optimization
    r = optimize.minimize(volume_from_angles,
                          best,
                          tol=angle_tol,
                          method='SLSQP',
                          bounds=bounds)

    tic.append(now())
    log.debug('Performed search in %f and minimize in %f', *np.diff(tic))

    # actually chunk the information about the cylinder
    transform, radius, height = volume_from_angles(
        r['x'], return_data=True)

    result = {'transform': transform,
              'radius': radius,
              'height': height}
    return result


def to_extents(bounds):
    """
    Convert an axis aligned bounding box to extents and
    transform.

    Parameters
    ------------
    bounds : (2, 3) float
      Axis aligned bounds in space

    Returns
    ------------
    extents : (3,) float
      Extents of the bounding box
    transform : (4, 4) float
      Homogeneous transform moving extents to bounds
    """
    bounds = np.asanyarray(bounds, dtype=np.float64)
    if bounds.shape != (2, 3):
        raise ValueError('bounds must be (2, 3)')

    extents = bounds.ptp(axis=0)
    transform = np.eye(4)
    transform[:3, 3] = bounds.mean(axis=0)

    return extents, transform


def corners(bounds):
    """
    Given a pair of axis aligned bounds, return all
    8 corners of the bounding box.

    Parameters
    ----------
    bounds : (2,3) or (2,2) float
      Axis aligned bounds

    Returns
    ----------
    corners : (8,3) float
      Corner vertices of the cube
    """

    bounds = np.asanyarray(bounds, dtype=np.float64)

    if util.is_shape(bounds, (2, 2)):
        bounds = np.column_stack((bounds, [0, 0]))
    elif not util.is_shape(bounds, (2, 3)):
        raise ValueError('bounds must be (2,2) or (2,3)!')

    minx, miny, minz, maxx, maxy, maxz = np.arange(6)
    corner_index = np.array([minx, miny, minz,
                             maxx, miny, minz,
                             maxx, maxy, minz,
                             minx, maxy, minz,
                             minx, miny, maxz,
                             maxx, miny, maxz,
                             maxx, maxy, maxz,
                             minx, maxy, maxz]).reshape((-1, 3))

    corners = bounds.reshape(-1)[corner_index]
    return corners


def contains(bounds, points):
    """
    Do an axis aligned bounding box check on a list of points.

    Parameters
    -----------
    bounds : (2, dimension) float
       Axis aligned bounding box
    points : (n, dimension) float
       Points in space

    Returns
    -----------
    points_inside : (n,) bool
      True if points are inside the AABB
    """
    # make sure we have correct input types
    bounds = np.asanyarray(bounds, dtype=np.float64)
    points = np.asanyarray(points, dtype=np.float64)

    if len(bounds) != 2:
        raise ValueError('bounds must be (2,dimension)!')
    if not util.is_shape(points, (-1, bounds.shape[1])):
        raise ValueError('bounds shape must match points!')

    # run the simple check
    points_inside = np.logical_and(
        (points > bounds[0]).all(axis=1),
        (points < bounds[1]).all(axis=1))

    return points_inside


# trimesh/caching.py

"""
caching.py
-----------

Functions and classes that help with tracking changes
in `numpy.ndarray` and clearing cached values based
on those changes.

You should really `pip install xxhash`:

```
In [23]: %timeit int(blake2b(d).hexdigest(), 16)
102 us +/- 684 ns per loop

In [24]: %timeit int(sha256(d).hexdigest(), 16)
142 us +/- 3.73 us

In [25]: %timeit xxh3_64_intdigest(d)
3.37 us +/- 116 ns per loop
```
"""
import os
import time
import warnings
import numpy as np

from functools import wraps
from .constants import log
from .util import is_sequence

try:
    from collections.abc import Mapping
except BaseException:
    from collections import Mapping


# sha256 is always available
from hashlib import sha256 as _sha256


def sha256(item):
    return int(_sha256(item).hexdigest(), 16)


try:
    # blake2b is available on Python 3 and
    from hashlib import blake2b as _blake2b

    def hash_fallback(item):
        return int(_blake2b(item).hexdigest(), 16)
except BaseException:
    # fallback to sha256
    hash_fallback = sha256

# xxhash is up to 30x faster than sha256:
# `pip install xxhash`
try:
    # newest version of algorithm
    from xxhash import xxh3_64_intdigest as hash_fast
except BaseException:
    try:
        # older version of the algorithm
        from xxhash import xxh64_intdigest as hash_fast
    except BaseException:
        # use hashlib as a fallback hashing library
        log.debug('falling back to hashlib ' +
                  'hashing: `pip install xxhash`' +
                  'for 50x faster cache checks')
        hash_fast = hash_fallback


def tracked_array(array, dtype=None):
    """
    Properly subclass a numpy ndarray to track changes.

    Avoids some pitfalls of subclassing by forcing contiguous
    arrays and does a view into a TrackedArray.

    Parameters
    ------------
    array : array- like object
      To be turned into a TrackedArray
    dtype : np.dtype
      Which dtype to use for the array

    Returns
    ------------
    tracked : TrackedArray
      Contains input array data.
    """
    # if someone passed us None, just create an empty array
    if array is None:
        array = []
    # make sure it is contiguous then view it as our subclass
    tracked = np.ascontiguousarray(
        array, dtype=dtype).view(TrackedArray)
    # should always be contiguous here
    assert tracked.flags['C_CONTIGUOUS']

    return tracked


def cache_decorator(function):
    """
    A decorator for class methods, replaces @property
    but will store and retrieve function return values
    in object cache.

    Parameters
    ------------
    function : method
      This is used as a decorator:
      ```
      @cache_decorator
      def foo(self, things):
        return 'happy days'
      ```
    """

    # use wraps to preserve docstring
    @wraps(function)
    def get_cached(*args, **kwargs):
        """
        Only execute the function if its value isn't stored
        in cache already.
        """
        self = args[0]
        # use function name as key in cache
        name = function.__name__
        # do the dump logic ourselves to avoid
        # verifying cache twice per call
        self._cache.verify()
        # access cache dict to avoid automatic validation
        # since we already called cache.verify manually
        if name in self._cache.cache:
            # already stored so return value
            return self._cache.cache[name]
        # value not in cache so execute the function
        value = function(*args, **kwargs)
        # store the value
        if self._cache.force_immutable and hasattr(
                value, 'flags') and len(value.shape) > 0:
            value.flags.writeable = False

        self._cache.cache[name] = value

        return value

    # all cached values are also properties
    # so they can be accessed like value attributes
    # rather than functions
    return property(get_cached)


class TrackedArray(np.ndarray):
    """
    Subclass of numpy.ndarray that provides hash methods
    to track changes.

    General method is to aggressively set 'modified' flags
    on operations which might (but don't necessarily) alter
    the array, ideally we sometimes compute hashes when we
    don't need to, but we don't return wrong hashes ever.

    We store boolean modified flag for each hash type to
    make checks fast even for queries of different hashes.

    Methods
    ----------
    __hash__ : int
      Runs the fastest available hash in this order:
        `xxh3_64, xxh_64, blake2b, sha256`
    """

    def __array_finalize__(self, obj):
        """
        Sets a modified flag on every TrackedArray
        This flag will be set on every change as well as
        during copies and certain types of slicing.
        """

        self._dirty_hash = True
        if isinstance(obj, type(self)):
            obj._dirty_hash = True

    def __array_wrap__(self, out_arr, context=None):
        """
        Return a numpy scalar if array is 0d.
        See https://github.com/numpy/numpy/issues/5819
        """
        if out_arr.ndim:
            return np.ndarray.__array_wrap__(
                self, out_arr, context)
        # Match numpy's behavior and return a numpy dtype scalar
        return out_arr[()]

    @property
    def mutable(self):
        return self.flags['WRITEABLE']

    @mutable.setter
    def mutable(self, value):
        self.flags.writeable = value

    def hash(self):
        warnings.warn(
            '`array.hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `array.__hash__()` or `hash(array)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def crc(self):
        warnings.warn(
            '`array.crc()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `array.__hash__()` or `hash(array)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def md5(self):
        warnings.warn(
            '`array.md5()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `array.__hash__()` or `hash(array)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def __hash__(self):
        """
        Return a fast hash of the contents of the array.

        Returns
        -------------
        hash : long int
          A hash of the array contents.
        """
        # repeat the bookkeeping to get a contiguous array
        if not self._dirty_hash and hasattr(self, '_hashed'):
            # we have a valid hash without recomputing.
            return self._hashed

        # run a hashing function on the C-order bytes copy
        hashed = hash_fast(self.tobytes(order='C'))

        # assign the value and set the flag
        self._hashed = hashed
        self._dirty_hash = False

        return hashed

    def __iadd__(self, *args, **kwargs):
        """
        In-place addition.

        The i* operations are in- place and modify the array,
        so we better catch all of them.
        """

        self._dirty_hash = True
        return super(self.__class__, self).__iadd__(
            *args, **kwargs)

    def __isub__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__isub__(
            *args, **kwargs)

    def fill(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).fill(
            *args, **kwargs)

    def partition(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).partition(
            *args, **kwargs)

    def put(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).put(
            *args, **kwargs)

    def byteswap(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).byteswap(
            *args, **kwargs)

    def itemset(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).itemset(
            *args, **kwargs)

    def sort(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).sort(
            *args, **kwargs)

    def setflags(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).setflags(
            *args, **kwargs)

    def __imul__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__imul__(
            *args, **kwargs)

    def __idiv__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__idiv__(
            *args, **kwargs)

    def __itruediv__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__itruediv__(
            *args, **kwargs)

    def __imatmul__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__imatmul__(
            *args, **kwargs)

    def __ipow__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__ipow__(
            *args, **kwargs)

    def __imod__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__imod__(
            *args, **kwargs)

    def __ifloordiv__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__ifloordiv__(
            *args, **kwargs)

    def __ilshift__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__ilshift__(
            *args, **kwargs)

    def __irshift__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__irshift__(
            *args, **kwargs)

    def __iand__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__iand__(
            *args, **kwargs)

    def __ixor__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__ixor__(
            *args, **kwargs)

    def __ior__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__ior__(
            *args, **kwargs)

    def __setitem__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__setitem__(
            *args, **kwargs)

    def __setslice__(self, *args, **kwargs):
        self._dirty_hash = True
        return super(self.__class__, self).__setslice__(
            *args, **kwargs)


class Cache(object):
    """
    Class to cache values which will be stored until the
    result of an ID function changes.
    """

    def __init__(self, id_function, force_immutable=False):
        """
        Create a cache object.

        Parameters
        ------------
        id_function : function
          Returns hashable value
        force_immutable : bool
          If set will make all numpy arrays read-only
        """
        self._id_function = id_function
        # for stored numpy arrays set `flags.writable = False`
        self.force_immutable = bool(force_immutable)
        # call the id function for initial value
        self.id_current = self._id_function()
        # a counter for locks
        self._lock = 0
        # actual store for data
        self.cache = {}

    def delete(self, key):
        """
        Remove a key from the cache.
        """
        if key in self.cache:
            self.cache.pop(key, None)

    def verify(self):
        """
        Verify that the cached values are still for the same
        value of id_function and delete all stored items if
        the value of id_function has changed.
        """
        # if we are in a lock don't check anything
        if self._lock != 0:
            return

        # check the hash of our data
        id_new = self._id_function()

        # things changed
        if id_new != self.id_current:
            if len(self.cache) > 0:
                log.debug('%d items cleared from cache: %s',
                          len(self.cache),
                          str(list(self.cache.keys())))
            # hash changed, so dump the cache
            # do it manually rather than calling clear()
            # as we are internal logic and can avoid function calls
            self.cache = {}
            # set the id to the new data hash
            self.id_current = id_new

    def clear(self, exclude=None):
        """
        Remove elements in the cache.

        Parameters
        -----------
        exclude : list
          List of keys in cache to not clear.
        """
        if exclude is None:
            self.cache = {}
        else:
            self.cache = {k: v for k, v in self.cache.items()
                          if k in exclude}

    def update(self, items):
        """
        Update the cache with a set of key, value pairs without
        checking id_function.
        """
        self.cache.update(items)

        if self.force_immutable:
            for v in self.cache.values():
                if hasattr(v, 'flags') and len(v.shape) > 0:
                    v.flags.writeable = False
        self.id_set()

    def id_set(self):
        """
        Set the current ID to the value of the ID function.
        """
        self.id_current = self._id_function()

    def __getitem__(self, key):
        """
        Get an item from the cache. If the item
        is not in the cache, it will return None

        Parameters
        -------------
        key : hashable
               Key in dict

        Returns
        -------------
        cached : object, or None
          Object that was stored
        """
        self.verify()
        if key in self.cache:
            return self.cache[key]
        return None

    def __setitem__(self, key, value):
        """
        Add an item to the cache.

        Parameters
        ------------
        key : hashable
          Key to reference value
        value : any
          Value to store in cache
        """
        # dumpy cache if ID function has changed
        self.verify()
        # make numpy arrays read-only if asked to
        if self.force_immutable and hasattr(
                value, 'flags') and len(value.shape) > 0:
            value.flags.writeable = False
        # assign data to dict
        self.cache[key] = value

        return value

    def __contains__(self, key):
        self.verify()
        return key in self.cache

    def __len__(self):
        self.verify()
        return len(self.cache)

    def __enter__(self):
        self._lock += 1

    def __exit__(self, *args):
        self._lock -= 1
        self.id_current = self._id_function()


class DiskCache(object):
    """
    Store results of expensive operations on disk
    with an option to expire the results. This is used
    to cache the multi-gigabyte test corpuses in
    `tests/corpus.py`
    """

    def __init__(self, path, expire_days=30):
        """
        Create a cache on disk for storing expensive results.

        Parameters
        --------------
        path : str
          A writeable location on the current file path.
        expire_days : int or float
          How old should results be considered expired.

        """
        # store how old we allow results to be
        self.expire_days = expire_days
        # store the location for saving results
        self.path = os.path.abspath(
            os.path.expanduser(path))
        # make sure the specified path exists
        os.makedirs(self.path, exist_ok=True)

    def get(self, key, fetch):
        """
        Get a key from the cache or run a calculation.

        Parameters
        -----------
        key : str
          Key to reference item with
        fetch : function
          If key isn't stored and recent run this
          function and store its result on disk.
        """
        # hash the key so we have a fixed length string
        key_hash = _sha256(key.encode('utf-8')).hexdigest()
        # full path of result on local disk
        path = os.path.join(self.path, key_hash)

        # check to see if we can use the cache
        if os.path.isfile(path):
            # compute the age of the existing file in days
            age_days = (time.time() - os.stat(path).st_mtime) / 86400.0
            if age_days < self.expire_days:
                # this nested condition means that
                # the file both exists and is recent
                # enough, so just return its contents
                with open(path, 'rb') as f:
                    return f.read()

        log.debug('not in cache fetching: `{}`'.format(key))
        # since we made it here our data isn't cached
        # run the expensive function to fetch the file
        raw = fetch()
        # write the data so we can save it
        with open(path, 'wb') as f:
            f.write(raw)

        # return the data
        return raw


class DataStore(Mapping):
    """
    A class to store multiple numpy arrays and track them all
    for changes.

    Operates like a dict that only stores numpy.ndarray
    """

    def __init__(self):
        self.data = {}

    def __iter__(self):
        return iter(self.data)

    def pop(self, key):
        return self.data.pop(key, None)

    def __delitem__(self, key):
        self.data.pop(key, None)

    @property
    def mutable(self):
        """
        Is data allowed to be altered or not.

        Returns
        -----------
        is_mutable : bool
          Can data be altered in the DataStore
        """
        return getattr(self, '_mutable', True)

    @mutable.setter
    def mutable(self, value):
        """
        Is data allowed to be altered or not.

        Parameters
        ------------
        is_mutable : bool
          Should data be allowed to be altered
        """
        # make sure passed value is a bool
        is_mutable = bool(value)
        # apply the flag to any data stored
        for v in self.data.values():
            v.mutable = value
        # save the mutable setting
        self._mutable = is_mutable

    def is_empty(self):
        """
        Is the current DataStore empty or not.

        Returns
        ----------
        empty : bool
          False if there are items in the DataStore
        """
        if len(self.data) == 0:
            return True
        for v in self.data.values():
            if is_sequence(v):
                if len(v) == 0:
                    return True
                else:
                    return False
            elif bool(np.isreal(v)):
                return False
        return True

    def clear(self):
        """
        Remove all data from the DataStore.
        """
        self.data = {}

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, data):
        """
        Store an item in the DataStore
        """
        # we shouldn't allow setting on immutable datastores
        if not self.mutable:
            raise ValueError('DataStore is configured immutable!')

        if hasattr(data, 'hash'):
            # don't bother to re-track TrackedArray
            tracked = data
        else:
            # otherwise wrap data
            tracked = tracked_array(data)
        # apply our mutability setting

        if hasattr(self, '_mutable'):
            # apply our mutability setting only if it was explicitly set
            tracked.mutable = self.mutable
        # store data
        self.data[key] = tracked

    def __contains__(self, key):
        return key in self.data

    def __len__(self):
        return len(self.data)

    def update(self, values):
        if not isinstance(values, dict):
            raise ValueError('Update only implemented for dicts')
        for key, value in values.items():
            self[key] = value

    def __hash__(self):
        """
        Get a hash reflecting everything in the DataStore.

        Returns
        ----------
        hash : str
          hash of data in hexadecimal
        """
        # only hash values that aren't None
        # or if they are arrays require length greater than zero
        return hash_fast(np.array(
            [hash(v) for v in self.data.values()
             if v is not None and
             (not hasattr(v, '__len__') or len(v) > 0)],
            dtype=np.int64).tobytes())

    def crc(self):
        """
        Get a CRC reflecting everything in the DataStore.

        Returns
        ----------
        crc : int
          CRC of data
        """
        warnings.warn(
            '`array.crc()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `array.__hash__()` or `hash(array)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def fast_hash(self):
        """
        Get a CRC32 or xxhash.xxh64 reflecting the DataStore.

        Returns
        ------------
        hashed : int
          Checksum of data
        """
        warnings.warn(
            '`array.fast_hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `array.__hash__()` or `hash(array)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def hash(self):
        warnings.warn(
            '`array.hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `array.__hash__()` or `hash(array)`',
            category=DeprecationWarning, stacklevel=2)

        return self.__hash__()


# trimesh/collision.py

import numpy as np

import collections

try:
    # pip install python-fcl
    import fcl
except BaseException:
    fcl = None


class ContactData(object):
    """
    Data structure for holding information about a collision contact.
    """

    def __init__(self, names, contact):
        """
        Initialize a ContactData.

        Parameters
        ----------
        names : list of str
          The names of the two objects in order.
        contact : fcl.Contact
          The contact in question.
        """
        self.names = set(names)
        self._inds = {
            names[0]: contact.b1,
            names[1]: contact.b2
        }
        self._normal = contact.normal
        self._point = contact.pos
        self._depth = contact.penetration_depth

    @property
    def normal(self):
        """
        The 3D intersection normal for this contact.

        Returns
        -------
        normal : (3,) float
          The intersection normal.
        """
        return self._normal

    @property
    def point(self):
        """
        The 3D point of intersection for this contact.

        Returns
        -------
        point : (3,) float
          The intersection point.
        """
        return self._point

    @property
    def depth(self):
        """
        The penetration depth of the 3D point of intersection for this contact.

        Returns
        -------
        depth : float
          The penetration depth.
        """
        return self._depth

    def index(self, name):
        """
        Returns the index of the face in contact for the mesh with
        the given name.

        Parameters
        ----------
        name : str
          The name of the target object.

        Returns
        -------
        index : int
          The index of the face in collision
        """
        return self._inds[name]


class DistanceData(object):
    """
    Data structure for holding information about a distance query.
    """

    def __init__(self, names, result):
        """
        Initialize a DistanceData.

        Parameters
        ----------
        names : list of str
          The names of the two objects in order.
        contact : fcl.DistanceResult
          The distance query result.
        """
        self.names = set(names)
        self._inds = {
            names[0]: result.b1,
            names[1]: result.b2
        }
        self._points = {
            names[0]: result.nearest_points[0],
            names[1]: result.nearest_points[1]
        }
        self._distance = result.min_distance

    @property
    def distance(self):
        """
        Returns the distance between the two objects.

        Returns
        -------
        distance : float
          The euclidean distance between the objects.
        """
        return self._distance

    def index(self, name):
        """
        Returns the index of the closest face for the mesh with
        the given name.

        Parameters
        ----------
        name : str
          The name of the target object.

        Returns
        -------
        index : int
          The index of the face in collisoin.
        """
        return self._inds[name]

    def point(self, name):
        """
        The 3D point of closest distance on the mesh with the given name.

        Parameters
        ----------
        name : str
          The name of the target object.

        Returns
        -------
        point : (3,) float
          The closest point.
        """
        return self._points[name]


class CollisionManager(object):
    """
    A mesh-mesh collision manager.
    """

    def __init__(self):
        """
        Initialize a mesh-mesh collision manager.
        """
        if fcl is None:
            raise ValueError(
                'No FCL Available! Please install the python-fcl library')
        # {name: {geom:, obj}}
        self._objs = {}
        # {id(bvh) : str, name}
        # unpopulated values will return None
        self._names = collections.defaultdict(lambda: None)

        self._manager = fcl.DynamicAABBTreeCollisionManager()
        self._manager.setup()

    def add_object(self,
                   name,
                   mesh,
                   transform=None):
        """
        Add an object to the collision manager.

        If an object with the given name is already in the manager,
        replace it.

        Parameters
        ----------
        name : str
          An identifier for the object
        mesh : Trimesh object
          The geometry of the collision object
        transform : (4,4) float
          Homogeneous transform matrix for the object
        """

        # if no transform passed, assume identity transform
        if transform is None:
            transform = np.eye(4)
        transform = np.asanyarray(transform, dtype=np.float32)
        if transform.shape != (4, 4):
            raise ValueError('transform must be (4,4)!')

        # create BVH/Convex
        geom = self._get_fcl_obj(mesh)

        # create the FCL transform from (4,4) matrix
        t = fcl.Transform(transform[:3, :3], transform[:3, 3])
        o = fcl.CollisionObject(geom, t)

        # Add collision object to set
        if name in self._objs:
            self._manager.unregisterObject(self._objs[name])
        self._objs[name] = {'obj': o,
                            'geom': geom}
        # store the name of the geometry
        self._names[id(geom)] = name

        self._manager.registerObject(o)
        self._manager.update()
        return o

    def remove_object(self, name):
        """
        Delete an object from the collision manager.

        Parameters
        ----------
        name : str
          The identifier for the object
        """
        if name in self._objs:
            self._manager.unregisterObject(self._objs[name]['obj'])
            self._manager.update(self._objs[name]['obj'])
            # remove objects from _objs
            geom_id = id(self._objs.pop(name)['geom'])
            # remove names
            self._names.pop(geom_id)
        else:
            raise ValueError('{} not in collision manager!'.format(name))

    def set_transform(self, name, transform):
        """
        Set the transform for one of the manager's objects.
        This replaces the prior transform.

        Parameters
        ----------
        name : str
          An identifier for the object already in the manager
        transform : (4,4) float
          A new homogeneous transform matrix for the object
        """
        if name in self._objs:
            o = self._objs[name]['obj']
            o.setRotation(transform[:3, :3])
            o.setTranslation(transform[:3, 3])
            self._manager.update(o)
        else:
            raise ValueError('{} not in collision manager!'.format(name))

    def in_collision_single(self,
                            mesh,
                            transform=None,
                            return_names=False,
                            return_data=False):
        """
        Check a single object for collisions against all objects in the
        manager.

        Parameters
        ----------
        mesh : Trimesh object
          The geometry of the collision object
        transform : (4,4) float
          Homogeneous transform matrix
        return_names : bool
          If true, a set is returned containing the names
          of all objects in collision with the object
        return_data :  bool
          If true, a list of ContactData is returned as well

        Returns
        ------------
        is_collision : bool
          True if a collision occurs and False otherwise
        names : set of str
          [OPTIONAL] The set of names of objects that collided with the
          provided one
        contacts : list of ContactData
          [OPTIONAL] All contacts detected
        """
        if transform is None:
            transform = np.eye(4)

        # create BVH/Convex
        geom = self._get_fcl_obj(mesh)

        # create the FCL transform from (4,4) matrix
        t = fcl.Transform(transform[:3, :3], transform[:3, 3])
        o = fcl.CollisionObject(geom, t)

        # Collide with manager's objects
        cdata = fcl.CollisionData()
        if return_names or return_data:
            cdata = fcl.CollisionData(request=fcl.CollisionRequest(
                num_max_contacts=100000,
                enable_contact=True))

        self._manager.collide(o, cdata, fcl.defaultCollisionCallback)
        result = cdata.result.is_collision

        # If we want to return the objects that were collision, collect them.
        objs_in_collision = set()
        contact_data = []
        if return_names or return_data:
            for contact in cdata.result.contacts:
                cg = contact.o1
                if cg == geom:
                    cg = contact.o2
                name = self._extract_name(cg)

                names = (name, '__external')
                if cg == contact.o2:
                    names = tuple(reversed(names))

                if return_names:
                    objs_in_collision.add(name)
                if return_data:
                    contact_data.append(ContactData(names, contact))

        if return_names and return_data:
            return result, objs_in_collision, contact_data
        elif return_names:
            return result, objs_in_collision
        elif return_data:
            return result, contact_data
        else:
            return result

    def in_collision_internal(self, return_names=False, return_data=False):
        """
        Check if any pair of objects in the manager collide with one another.

        Parameters
        ----------
        return_names : bool
          If true, a set is returned containing the names
          of all pairs of objects in collision.
        return_data :  bool
          If true, a list of ContactData is returned as well

        Returns
        -------
        is_collision : bool
          True if a collision occurred between any pair of objects
          and False otherwise
        names : set of 2-tup
          The set of pairwise collisions. Each tuple
          contains two names in alphabetical order indicating
          that the two corresponding objects are in collision.
        contacts : list of ContactData
          All contacts detected
        """
        cdata = fcl.CollisionData()
        if return_names or return_data:
            cdata = fcl.CollisionData(request=fcl.CollisionRequest(
                num_max_contacts=100000, enable_contact=True))

        self._manager.collide(cdata, fcl.defaultCollisionCallback)

        result = cdata.result.is_collision

        objs_in_collision = set()
        contact_data = []
        if return_names or return_data:
            for contact in cdata.result.contacts:
                names = (self._extract_name(contact.o1),
                         self._extract_name(contact.o2))

                if return_names:
                    objs_in_collision.add(tuple(sorted(names)))
                if return_data:
                    contact_data.append(ContactData(names, contact))

        if return_names and return_data:
            return result, objs_in_collision, contact_data
        elif return_names:
            return result, objs_in_collision
        elif return_data:
            return result, contact_data
        else:
            return result

    def in_collision_other(self, other_manager,
                           return_names=False, return_data=False):
        """
        Check if any object from this manager collides with any object
        from another manager.

        Parameters
        -------------------
        other_manager : CollisionManager
          Another collision manager object
        return_names : bool
          If true, a set is returned containing the names
          of all pairs of objects in collision.
        return_data : bool
          If true, a list of ContactData is returned as well

        Returns
        -------------
        is_collision : bool
          True if a collision occurred between any pair of objects
          and False otherwise
        names : set of 2-tup
          The set of pairwise collisions. Each tuple
          contains two names (first from this manager,
          second from the other_manager) indicating
          that the two corresponding objects are in collision.
        contacts : list of ContactData
          All contacts detected
        """
        cdata = fcl.CollisionData()
        if return_names or return_data:
            cdata = fcl.CollisionData(
                request=fcl.CollisionRequest(
                    num_max_contacts=100000,
                    enable_contact=True))
        self._manager.collide(other_manager._manager,
                              cdata,
                              fcl.defaultCollisionCallback)
        result = cdata.result.is_collision

        objs_in_collision = set()
        contact_data = []
        if return_names or return_data:
            for contact in cdata.result.contacts:
                reverse = False
                names = (self._extract_name(contact.o1),
                         other_manager._extract_name(contact.o2))
                if names[0] is None:
                    names = (self._extract_name(contact.o2),
                             other_manager._extract_name(contact.o1))
                    reverse = True

                if return_names:
                    objs_in_collision.add(names)
                if return_data:
                    if reverse:
                        names = tuple(reversed(names))
                    contact_data.append(ContactData(names, contact))

        if return_names and return_data:
            return result, objs_in_collision, contact_data
        elif return_names:
            return result, objs_in_collision
        elif return_data:
            return result, contact_data
        else:
            return result

    def min_distance_single(self,
                            mesh,
                            transform=None,
                            return_name=False,
                            return_data=False):
        """
        Get the minimum distance between a single object and any
        object in the manager.

        Parameters
        ---------------
        mesh : Trimesh object
          The geometry of the collision object
        transform : (4,4) float
          Homogeneous transform matrix for the object
        return_names : bool
          If true, return name of the closest object
        return_data : bool
          If true, a DistanceData object is returned as well

        Returns
        -------------
        distance : float
          Min distance between mesh and any object in the manager
        name : str
          The name of the object in the manager that was closest
        data : DistanceData
          Extra data about the distance query
        """
        if transform is None:
            transform = np.eye(4)

        # create BVH/Convex
        geom = self._get_fcl_obj(mesh)

        # create the FCL transform from (4,4) matrix
        t = fcl.Transform(transform[:3, :3], transform[:3, 3])
        o = fcl.CollisionObject(geom, t)

        # Collide with manager's objects
        ddata = fcl.DistanceData(
            fcl.DistanceRequest(
                enable_signed_distance=True))
        if return_data:
            ddata = fcl.DistanceData(
                fcl.DistanceRequest(
                    enable_nearest_points=True,
                    enable_signed_distance=True
                ),
                fcl.DistanceResult()
            )

        self._manager.distance(o, ddata, fcl.defaultDistanceCallback)

        distance = ddata.result.min_distance

        # If we want to return the objects that were collision, collect them.
        name, data = None, None
        if return_name or return_data:
            cg = ddata.result.o1
            if cg == geom:
                cg = ddata.result.o2

            name = self._extract_name(cg)

            names = (name, '__external')
            if cg == ddata.result.o2:
                names = tuple(reversed(names))
            data = DistanceData(names, ddata.result)

        if return_name and return_data:
            return distance, name, data
        elif return_name:
            return distance, name
        elif return_data:
            return distance, data
        else:
            return distance

    def min_distance_internal(self, return_names=False, return_data=False):
        """
        Get the minimum distance between any pair of objects in the manager.

        Parameters
        -------------
        return_names : bool
          If true, a 2-tuple is returned containing the names
          of the closest objects.
        return_data : bool
          If true, a DistanceData object is returned as well

        Returns
        -----------
        distance : float
          Min distance between any two managed objects
        names : (2,) str
          The names of the closest objects
        data : DistanceData
          Extra data about the distance query
        """
        ddata = fcl.DistanceData(
            fcl.DistanceRequest(
                enable_signed_distance=True))
        if return_data:
            ddata = fcl.DistanceData(
                fcl.DistanceRequest(
                    enable_nearest_points=True,
                    enable_signed_distance=True,
                ),
                fcl.DistanceResult()
            )

        self._manager.distance(ddata, fcl.defaultDistanceCallback)

        distance = ddata.result.min_distance

        names, data = None, None
        if return_names or return_data:
            names = (self._extract_name(ddata.result.o1),
                     self._extract_name(ddata.result.o2))
            data = DistanceData(names, ddata.result)
            names = tuple(sorted(names))

        if return_names and return_data:
            return distance, names, data
        elif return_names:
            return distance, names
        elif return_data:
            return distance, data
        else:
            return distance

    def min_distance_other(self, other_manager,
                           return_names=False, return_data=False):
        """
        Get the minimum distance between any pair of objects,
        one in each manager.

        Parameters
        ----------
        other_manager : CollisionManager
          Another collision manager object
        return_names : bool
          If true, a 2-tuple is returned containing
          the names of the closest objects.
        return_data : bool
          If true, a DistanceData object is returned as well

        Returns
        -----------
        distance : float
          The min distance between a pair of objects,
          one from each manager.
        names : 2-tup of str
          A 2-tuple containing two names (first from this manager,
          second from the other_manager) indicating
          the two closest objects.
        data : DistanceData
          Extra data about the distance query
        """
        ddata = fcl.DistanceData(
            fcl.DistanceRequest(
                enable_signed_distance=True))
        if return_data:
            ddata = fcl.DistanceData(
                fcl.DistanceRequest(
                    enable_nearest_points=True,
                    enable_signed_distance=True,
                ),
                fcl.DistanceResult()
            )

        self._manager.distance(other_manager._manager,
                               ddata,
                               fcl.defaultDistanceCallback)

        distance = ddata.result.min_distance

        names, data = None, None
        if return_names or return_data:
            reverse = False
            names = (self._extract_name(ddata.result.o1),
                     other_manager._extract_name(ddata.result.o2))
            if names[0] is None:
                reverse = True
                names = (self._extract_name(ddata.result.o2),
                         other_manager._extract_name(ddata.result.o1))

            dnames = tuple(names)
            if reverse:
                dnames = tuple(reversed(dnames))
            data = DistanceData(dnames, ddata.result)

        if return_names and return_data:
            return distance, names, data
        elif return_names:
            return distance, names
        elif return_data:
            return distance, data
        else:
            return distance

    def _get_fcl_obj(self, mesh):
        """
        Get a BVH or Convex for a mesh.

        Parameters
        -------------
        mesh : Trimesh
          Mesh to create BVH/Convex for

        Returns
        --------------
        obj : fcl.BVHModel or fcl.Convex
          BVH/Convex object of source mesh
        """

        if mesh.is_convex:
            obj = mesh_to_convex(mesh)
        else:
            obj = mesh_to_BVH(mesh)
        return obj

    def _extract_name(self, geom):
        """
        Retrieve the name of an object from the manager by its
        CollisionObject, or return None if not found.

        Parameters
        -----------
        geom : CollisionObject or BVHModel
          Input model

        Returns
        ------------
        names : hashable
          Name of input geometry
        """
        return self._names[id(geom)]


def mesh_to_BVH(mesh):
    """
    Create a BVHModel object from a Trimesh object

    Parameters
    -----------
    mesh : Trimesh
      Input geometry

    Returns
    ------------
    bvh : fcl.BVHModel
      BVH of input geometry
    """
    bvh = fcl.BVHModel()
    bvh.beginModel(num_tris_=len(mesh.faces),
                   num_vertices_=len(mesh.vertices))
    bvh.addSubModel(verts=mesh.vertices,
                    triangles=mesh.faces)
    bvh.endModel()
    return bvh


def mesh_to_convex(mesh):
    """
    Create a Convex object from a Trimesh object

    Parameters
    -----------
    mesh : Trimesh
      Input geometry

    Returns
    ------------
    convex : fcl.Convex
      Convex of input geometry
    """
    fs = np.concatenate((3 * np.ones((len(mesh.faces), 1), dtype=np.int64), mesh.faces),
                        axis=1)
    return fcl.Convex(mesh.vertices, len(fs), fs.flatten())


def scene_to_collision(scene):
    """
    Create collision objects from a trimesh.Scene object.

    Parameters
    ------------
    scene : trimesh.Scene
      Scene to create collision objects for

    Returns
    ------------
    manager : CollisionManager
      CollisionManager for objects in scene
    objects: {node name: CollisionObject}
      Collision objects for nodes in scene
    """
    manager = CollisionManager()
    objects = {}
    for node in scene.graph.nodes_geometry:
        T, geometry = scene.graph[node]
        objects[node] = manager.add_object(name=node,
                                           mesh=scene.geometry[geometry],
                                           transform=T)
    return manager, objects


# trimesh/comparison.py

"""
comparison.py
----------------

Provide methods for quickly hashing and comparing meshes.
"""

import numpy as np

from . import util

from hashlib import sha256
from .constants import tol

# how many significant figures to use for each
# field of the identifier based on hand-tuning
id_sigfig = np.array(
    [5,   # area
     10,  # euler number
     5,  # area/volume ratio
     2,  # convex/mesh area ratio
     2,  # convex area/volume ratio
     3,  # max radius squared / area
     1])  # sign of triangle count for mirrored


def identifier_simple(mesh):
    """
    Return a basic identifier for a mesh consisting of
    properties that have been hand tuned to be somewhat
    robust to rigid transformations and different
    tesselations.

    Parameters
    ------------
    mesh : trimesh.Trimesh
      Source geometry

    Returns
    ----------
    identifier : (7,) float
      Identifying values of the mesh
    """
    # verify the cache once
    mesh._cache.verify()

    # don't check hashes during identifier as we aren't
    # changing any data values of the mesh inside block
    # if we did change values in cache block things would break
    with mesh._cache:
        # pre-allocate identifier so indexes of values can't move around
        # like they might if we used hstack or something else
        identifier = np.zeros(7, dtype=np.float64)
        # avoid thrashing the cache unnecessarily
        mesh_area = mesh.area
        # start with properties that are valid regardless of watertightness
        # note that we're going to try to make all parameters relative
        # to area so other values don't get blown up at weird scales
        identifier[0] = mesh_area
        # avoid divide-by-zero later
        if mesh_area < tol.merge:
            mesh_area = 1.0
        # topological constant and the only thing we can really
        # trust in this fallen world
        identifier[1] = mesh.euler_number

        # if we have a watertight mesh include volume and inertia
        if mesh.is_volume:
            # side length of a cube ratio
            # 1.0 for cubes, different values for other things
            identifier[2] = (((mesh_area / 6.0) ** (1.0 / 2.0)) /
                             (mesh.volume ** (1.0 / 3.0)))
        else:
            # if we don't have a watertight mesh add information about the
            # convex hull which is slow to compute and unreliable
            try:
                # get the hull area and volume
                hull = mesh.convex_hull
                hull_area = hull.area
                hull_volume = hull.volume
            except BaseException:
                # in-plane or single point geometry has no hull
                hull_area = 6.0
                hull_volume = 1.0
            # just what we're looking for in a hash but hey
            identifier[3] = mesh_area / hull_area
            # cube side length ratio for the hull
            if hull_volume > 1e-12:
                identifier[4] = (((hull_area / 6.0) ** (1.0 / 2.0)) /
                                 (hull_volume ** (1.0 / 3.0)))
            # calculate maximum mesh radius
            vertices = mesh.vertices - mesh.centroid
            # add in max radius^2 to area ratio
            R2 = np.dot((vertices ** 2), [1, 1, 1]).max()
            identifier[5] = R2 / mesh_area

    # mirrored meshes will look identical in terms of
    # area, volume, etc: use a count of relative edge
    # lengths to differentiate identical but mirrored meshes
    # this doesn't work well on meshes with a small number of faces
    if len(mesh.faces) > 50:
        # does this mesh have edges that differ substantially in length
        # if not this method for detecting reflection will not work
        # and the result will definitely be garbage
        edges_length = mesh.edges_unique_length
        variance = edges_length.std() / edges_length.mean()
        if variance > 0.25:
            # the length of each edge in faces
            norms = edges_length[
                mesh.edges_unique_inverse].reshape((-1, 3))
            # stack edge length and get the relative difference
            stack = np.diff(np.column_stack((norms, norms[:, 0])), axis=1)
            pick_idx = np.abs(stack).argmin(axis=1)
            # get the edge length diff
            pick = stack.reshape(-1)[pick_idx + (np.arange(len(pick_idx)) * 3)]
            # reduce to the bare minimum that tests stable
            identifier[6] = np.sign(pick.sum())
    return identifier


def identifier_hash(identifier):
    """
    Hash an identifier array in a way that is hand-tuned to be
    somewhat robust to likely changes.

    Parameters
    ------------
    identifier : (n,) float
      Vector of properties

    Returns
    ----------
    hash : (64,) str
      A SHA256 of the identifier vector at hand-tuned precision.
    """

    # convert identifier to integers and order of magnitude
    as_int, multiplier = util.sigfig_int(
        identifier, id_sigfig)

    # make all scales positive
    if (multiplier < 0).any():
        multiplier += np.abs(multiplier.min())
    data = (as_int * (10 ** multiplier)).astype(np.int64)
    return sha256(data.tobytes()).hexdigest()


# trimesh/constants.py

import numpy as np

from .util import log, now


class ToleranceMesh(object):
    """
    ToleranceMesh objects hold tolerance information about meshes.

    Parameters
    ----------------
    tol.zero : float
      Floating point numbers smaller than this are considered zero
    tol.merge : float
      When merging vertices, consider vertices closer than this
      to be the same vertex. Here we use the same value (1e-8)
      as SolidWorks uses, according to their documentation.
    tol.planar : float
      The maximum distance from a plane a point can be and
      still be considered to be on the plane
    tol.facet_threshold : float
      Threshold for two facets to be considered coplanar
    tol.strict : bool
      If True, run additional in- process checks (slower)
    """

    def __init__(self, **kwargs):
        # set our zero for floating point comparison to 100x
        # the resolution of float64 which works out to 1e-13
        self.zero = np.finfo(np.float64).resolution * 100
        # vertices closer than this should be merged
        self.merge = 1e-8
        # peak to valley flatness to be considered planar
        self.planar = 1e-5
        # coplanar threshold: ratio of (radius / span) ** 2
        self.facet_threshold = 5000
        # run additional checks and asserts
        self.strict = False

        # add any passed kwargs
        self.__dict__.update(kwargs)


class TolerancePath(object):
    """
    TolerancePath objects contain tolerance information used in
    Path objects.

    Parameters
    ---------------
    tol.zero : float
      Floating point numbers smaller than this are considered zero
    tol.merge : float
      When merging vertices, consider vertices closer than this
      to be the same vertex. Here we use the same value (1e-8)
      as SolidWorks uses, according to their documentation.
    tol.planar : float
      The maximum distance from a plane a point can be and
      still be considered to be on the plane
    tol.seg_frac : float
      When simplifying line segments what percentage of the drawing
       scale can a segment be and have a curve fitted
    tol.seg_angle : float
      When simplifying line segments to arcs, what angle
      can a segment span to be acceptable.
    tol.aspect_frac : float
      When simplifying line segments to closed arcs (circles)
      what percentage can the aspect ratio differfrom 1:1
      before escaping the fit early
    tol.radius_frac : float
      When simplifying line segments to arcs, what percentage
      of the fit radius can vertices deviate to be acceptable
    tol.radius_min :
       When simplifying line segments to arcs, what is the minimum
       radius multiplied by document scale for an acceptable fit
    tol.radius_max :
       When simplifying line segments to arcs, what is the maximum
       radius multiplied by document scale for an acceptable fit
    tol.tangent :
       When simplifying line segments to curves, what is the maximum
       angle the end sections can deviate from tangent that is
       acceptable.
    """

    def __init__(self, **kwargs):
        # default values
        self.zero = 1e-12
        self.merge = 1e-5
        self.planar = 1e-5
        self.buffer = .05
        self.seg_frac = .125
        self.seg_angle = np.radians(50)
        self.seg_angle_min = np.radians(1)
        self.seg_angle_frac = .5
        self.aspect_frac = .1
        self.radius_frac = .02
        self.radius_min = 1e-4
        self.radius_max = 50
        self.tangent = np.radians(20)
        # run additional checks and asserts
        self.strict = False
        self.__dict__.update(kwargs)


class ResolutionPath(object):
    """
    res.seg_frac : float
      When discretizing curves, what percentage of the drawing
      scale should we aim to make a single segment
    res.seg_angle : float
      When discretizing curves, what angle should a section span
    res.max_sections : int
      When discretizing splines, what is the maximum number
      of segments per control point
    res.min_sections : int
      When discretizing splines, what is the minimum number
      of segments per control point
    res.export : str
      Format string to use when exporting floating point vertices
    """

    def __init__(self, **kwargs):
        self.seg_frac = .05
        self.seg_angle = .08
        self.max_sections = 500
        self.min_sections = 20
        self.export = '0.10f'


# instantiate mesh tolerances with defaults
tol = ToleranceMesh()

# instantiate path tolerances with defaults
tol_path = TolerancePath()
res_path = ResolutionPath()


def log_time(method):
    """
    A decorator for methods which will time the method
    and then emit a log.debug message with the method name
    and how long it took to execute.
    """
    def timed(*args, **kwargs):
        tic = now()
        result = method(*args, **kwargs)
        log.debug('%s executed in %.4f seconds.',
                  method.__name__,
                  now() - tic)

        return result
    timed.__name__ = method.__name__
    timed.__doc__ = method.__doc__
    return timed


# trimesh/convex.py

"""
convex.py

Deal with creating and checking convex objects in 2, 3 and N dimensions.

Convex is defined as:
1) "Convex, meaning "curving out" or "extending outward" (compare to concave)
2) having an outline or surface curved like the exterior of a circle or sphere.
3) (of a polygon) having only interior angles measuring less than 180
"""

import numpy as np

from .constants import tol

from . import util
from . import triangles


try:
    from scipy.spatial import ConvexHull
except ImportError as E:
    from .exceptions import ExceptionWrapper
    ConvexHull = ExceptionWrapper(E)

try:
    from scipy.spatial import QhullError
except BaseException:
    QhullError = BaseException


def convex_hull(obj, qhull_options='QbB Pp Qt', repair=True):
    """
    Get a new Trimesh object representing the convex hull of the
    current mesh attempting to return a watertight mesh with correct
    normals.

    Details on qhull options:
      http://www.qhull.org/html/qh-quick.htm#options

    Arguments
    --------
    obj : Trimesh, or (n,3) float
      Mesh or cartesian points
    qhull_options : str
      Options to pass to qhull.

    Returns
    --------
    convex : Trimesh
      Mesh of convex hull
    """
    from .base import Trimesh

    if isinstance(obj, Trimesh):
        points = obj.vertices.view(np.ndarray)
    else:
        # will remove subclassing
        points = np.asarray(obj, dtype=np.float64)
        if not util.is_shape(points, (-1, 3)):
            raise ValueError('Object must be Trimesh or (n,3) points!')

    try:
        hull = ConvexHull(points, qhull_options=qhull_options)
    except QhullError:
        util.log.debug(
            'Failed to compute convex hull: retrying with `QJ`',
            exc_info=True)
        # try with "joggle" enabled
        hull = ConvexHull(points, qhull_options='QJ')

    # hull object doesn't remove unreferenced vertices
    # create a mask to re- index faces for only referenced vertices
    vid = np.sort(hull.vertices)
    mask = np.zeros(len(hull.points), dtype=np.int64)
    mask[vid] = np.arange(len(vid))
    # remove unreferenced vertices here
    faces = mask[hull.simplices].copy()
    # rescale vertices back to original size
    vertices = hull.points[vid].copy()

    if not repair:
        # create the Trimesh object for the convex hull
        return Trimesh(vertices=vertices,
                       faces=faces,
                       process=True,
                       validate=False)

    # qhull returns faces with random winding
    # calculate the returned normal of each face
    crosses = triangles.cross(vertices[faces])

    # qhull returns zero magnitude faces like an asshole
    normals, valid = util.unitize(crosses, check_valid=True)

    # remove zero magnitude faces
    faces = faces[valid]
    crosses = crosses[valid]

    # each triangle area and mean center
    triangles_area = triangles.area(crosses=crosses, sum=False)
    triangles_center = vertices[faces].mean(axis=1)

    # since the convex hull is (hopefully) convex, the vector from
    # the centroid to the center of each face
    # should have a positive dot product with the normal of that face
    # if it doesn't it is probably backwards
    # note that this sometimes gets screwed up by precision issues
    centroid = np.average(triangles_center,
                          weights=triangles_area,
                          axis=0)
    # a vector from the centroid to a point on each face
    test_vector = triangles_center - centroid
    # check the projection against face normals
    backwards = util.diagonal_dot(normals, test_vector) < 0.0

    # flip the winding outward facing
    faces[backwards] = np.fliplr(faces[backwards])
    # flip the normal
    normals[backwards] *= -1.0

    # save the work we did to the cache so it doesn't have to be recomputed
    initial_cache = {'triangles_cross': crosses,
                     'triangles_center': triangles_center,
                     'area_faces': triangles_area,
                     'centroid': centroid}

    # create the Trimesh object for the convex hull
    convex = Trimesh(vertices=vertices,
                     faces=faces,
                     face_normals=normals,
                     initial_cache=initial_cache,
                     process=True,
                     validate=False)

    # we did the gross case above, but sometimes precision issues
    # leave some faces backwards anyway
    # this call will exit early if the winding is consistent
    # and if not will fix it by traversing the adjacency graph
    convex.fix_normals(multibody=False)

    # sometimes the QbB option will cause precision issues
    # so try the hull again without it and
    # check for qhull_options is None to avoid infinite recursion
    if (qhull_options is not None and
            not convex.is_winding_consistent):
        return convex_hull(convex, qhull_options=None)

    return convex


def adjacency_projections(mesh):
    """
    Test if a mesh is convex by projecting the vertices of
    a triangle onto the normal of its adjacent face.

    Parameters
    ----------
    mesh : Trimesh
      Input geometry

    Returns
    ----------
    projection : (len(mesh.face_adjacency),) float
      Distance of projection of adjacent vertex onto plane
    """
    # normals and origins from the first column of face adjacency
    normals = mesh.face_normals[mesh.face_adjacency[:, 0]]
    # one of the vertices on the shared edge
    origins = mesh.vertices[mesh.face_adjacency_edges[:, 0]]

    # faces from the second column of face adjacency
    vid_other = mesh.face_adjacency_unshared[:, 1]
    vector_other = mesh.vertices[vid_other] - origins

    # get the projection with a dot product
    dots = util.diagonal_dot(vector_other, normals)

    return dots


def is_convex(mesh):
    """
    Check if a mesh is convex.

    Parameters
    -----------
    mesh : Trimesh
      Input geometry

    Returns
    -----------
    convex : bool
      Was passed mesh convex or not
    """
    # non-watertight meshes are not convex
    # meshes with multiple bodies are not convex
    if not mesh.is_watertight or mesh.body_count != 1:
        return False

    # don't consider zero- area faces
    nonzero = mesh.area_faces > tol.zero
    # adjacencies with two nonzero faces
    adj_ok = nonzero[mesh.face_adjacency].all(axis=1)

    # if none of our face pairs are both nonzero exit
    # TODO : is this the correct check?
    # or should we just compare the projections
    # to the mesh scale
    if not adj_ok.any():
        return False

    # make threshold of convexity scale- relative
    threshold = tol.planar * mesh.scale

    # if projections of vertex onto plane of adjacent
    # face is negative, it means the face pair is locally
    # convex, and if that is true for all faces the mesh is convex
    convex = bool(mesh.face_adjacency_projections[adj_ok].max() < threshold)

    return convex


def hull_points(obj, qhull_options='QbB Pp'):
    """
    Try to extract a convex set of points from multiple input formats.

    Parameters
    ---------
    obj: Trimesh object
         (n,d) points
         (m,) Trimesh objects

    Returns
    --------
    points: (o,d) convex set of points
    """
    if hasattr(obj, 'convex_hull'):
        return obj.convex_hull.vertices

    initial = np.asanyarray(obj, dtype=np.float64)
    if len(initial.shape) != 2:
        raise ValueError('points must be (n, dimension)!')
    hull = ConvexHull(initial, qhull_options=qhull_options)
    points = hull.points[hull.vertices]

    return points


# trimesh/creation.py

"""
creation.py
--------------

Create meshes from primitives, or with operations.
"""

from .base import Trimesh
from .constants import log, tol
from .geometry import (faces_to_edges,
                       align_vectors,
                       plane_transform)

from . import util
from . import grouping
from . import triangles
from . import exceptions
from . import transformations as tf

import numpy as np

import warnings
import collections

try:
    # shapely is a soft dependency
    from shapely.geometry import Polygon
    from shapely.wkb import loads as load_wkb
except BaseException as E:
    # re-raise the exception when someone tries
    # to use the module that they don't have
    Polygon = exceptions.ExceptionWrapper(E)
    load_wkb = exceptions.ExceptionWrapper(E)

try:
    from mapbox_earcut import triangulate_float64 as _tri_earcut
except BaseException as E:
    _tri_earcut = exceptions.ExceptionWrapper(E)


def revolve(linestring,
            angle=None,
            sections=None,
            transform=None,
            **kwargs):
    """
    Revolve a 2D line string around the 2D Y axis, with a result with
    the 2D Y axis pointing along the 3D Z axis.

    This function is intended to handle the complexity of indexing
    and is intended to be used to create all radially symmetric primitives,
    eventually including cylinders, annular cylinders, capsules, cones,
    and UV spheres.

    Note that if your linestring is closed, it needs to be counterclockwise
    if you would like face winding and normals facing outwards.

    Parameters
    -------------
    linestring : (n, 2) float
      Lines in 2D which will be revolved
    angle : None or float
      Angle in radians to revolve curve by
    sections : None or int
      Number of sections result should have
      If not specified default is 32 per revolution
    transform : None or (4, 4) float
      Transform to apply to mesh after construction
    **kwargs : dict
      Passed to Trimesh constructor

    Returns
    --------------
    revolved : Trimesh
      Mesh representing revolved result
    """
    linestring = np.asanyarray(linestring, dtype=np.float64)

    # linestring must be ordered 2D points
    if len(linestring.shape) != 2 or linestring.shape[1] != 2:
        raise ValueError('linestring must be 2D!')

    if angle is None:
        # default to closing the revolution
        angle = np.pi * 2
        closed = True
    else:
        # check passed angle value
        closed = angle >= ((np.pi * 2) - 1e-8)

    if sections is None:
        # default to 32 sections for a full revolution
        sections = int(angle / (np.pi * 2) * 32)
    # change to face count
    sections += 1
    # create equally spaced angles
    theta = np.linspace(0, angle, sections)

    # 2D points around the revolution
    points = np.column_stack((np.cos(theta), np.sin(theta)))

    # how many points per slice
    per = len(linestring)
    # use the 2D X component as radius
    radius = linestring[:, 0]
    # use the 2D Y component as the height along revolution
    height = linestring[:, 1]
    # a lot of tiling to get our 3D vertices
    vertices = np.column_stack((
        np.tile(points, (1, per)).reshape((-1, 2)) *
        np.tile(radius, len(points)).reshape((-1, 1)),
        np.tile(height, len(points))))

    if closed:
        # should be a duplicate set of vertices
        assert np.allclose(vertices[:per],
                           vertices[-per:])
        # chop off duplicate vertices
        vertices = vertices[:-per]

    if transform is not None:
        # apply transform to vertices
        vertices = tf.transform_points(vertices, transform)

    # how many slices of the pie
    slices = len(theta) - 1

    # start with a quad for every segment
    # this is a superset which will then be reduced
    quad = np.array([0, per, 1,
                     1, per, per + 1])
    # stack the faces for a single slice of the revolution
    single = np.tile(quad, per).reshape((-1, 3))
    # `per` is basically the stride of the vertices
    single += np.tile(np.arange(per), (2, 1)).T.reshape((-1, 1))
    # remove any zero-area triangle
    # this covers many cases without having to think too much
    single = single[triangles.area(vertices[single]) > tol.merge]

    # how much to offset each slice
    # note arange multiplied by vertex stride
    # but tiled by the number of faces we actually have
    offset = np.tile(np.arange(slices) * per,
                     (len(single), 1)).T.reshape((-1, 1))
    # stack a single slice into N slices
    stacked = np.tile(single.ravel(), slices).reshape((-1, 3))

    if tol.strict:
        # make sure we didn't screw up stacking operation
        assert np.allclose(
            stacked.reshape(
                (-1, single.shape[0], 3)) - single, 0)

    # offset stacked and wrap vertices
    faces = (stacked + offset) % len(vertices)

    # create the mesh from our vertices and faces
    mesh = Trimesh(vertices=vertices,
                   faces=faces,
                   **kwargs)

    # strict checks run only in unit tests
    if (tol.strict and
            (np.allclose(radius[[0, -1]], 0.0) or
             np.allclose(linestring[0], linestring[-1]))):
        # if revolved curve starts and ends with zero radius
        # it should really be a valid volume, unless the sign
        # reversed on the input linestring
        assert mesh.is_volume
        assert mesh.body_count == 1

    return mesh


def extrude_polygon(polygon,
                    height,
                    transform=None,
                    **kwargs):
    """
    Extrude a 2D shapely polygon into a 3D mesh

    Parameters
    ----------
    polygon : shapely.geometry.Polygon
      2D geometry to extrude
    height : float
      Distance to extrude polygon along Z
    triangle_args : str or None
      Passed to triangle
    **kwargs : dict
      Passed to `triangulate_polygon`

    Returns
    ----------
    mesh : trimesh.Trimesh
      Resulting extrusion as watertight body
    """
    # create a triangulation from the polygon
    vertices, faces = triangulate_polygon(polygon, **kwargs)
    # extrude that triangulation along Z
    mesh = extrude_triangulation(vertices=vertices,
                                 faces=faces,
                                 height=height,
                                 transform=transform,
                                 **kwargs)
    return mesh


def sweep_polygon(polygon,
                  path,
                  angles=None,
                  **kwargs):
    """
    Extrude a 2D shapely polygon into a 3D mesh along an
    arbitrary 3D path. Doesn't handle sharp curvature well.


    Parameters
    ----------
    polygon : shapely.geometry.Polygon
      Profile to sweep along path
    path : (n, 3) float
      A path in 3D
    angles :  (n,) float
      Optional rotation angle relative to prior vertex
      at each vertex
    **kwargs : dict
      Passed to `triangulate_polygon`.
    Returns
    -------
    mesh : trimesh.Trimesh
      Geometry of result
    """

    path = np.asanyarray(path, dtype=np.float64)
    if not util.is_shape(path, (-1, 3)):
        raise ValueError('Path must be (n, 3)!')

    # Extract 2D vertices and triangulation
    verts_2d = np.array(polygon.exterior.coords)[:-1]
    base_verts_2d, faces_2d = triangulate_polygon(
        polygon, **kwargs)
    n = len(verts_2d)

    # Create basis for first planar polygon cap
    x, y, z = util.generate_basis(path[0] - path[1])
    tf_mat = np.ones((4, 4))
    tf_mat[:3, :3] = np.c_[x, y, z]
    tf_mat[:3, 3] = path[0]

    # Compute 3D locations of those vertices
    verts_3d = np.c_[verts_2d, np.zeros(n)]
    verts_3d = tf.transform_points(verts_3d, tf_mat)
    base_verts_3d = np.c_[base_verts_2d,
                          np.zeros(len(base_verts_2d))]
    base_verts_3d = tf.transform_points(base_verts_3d,
                                        tf_mat)

    # keep matching sequence of vertices and 0- indexed faces
    vertices = [base_verts_3d]
    faces = [faces_2d]

    # Compute plane normals for each turn --
    # each turn induces a plane halfway between the two vectors
    v1s = util.unitize(path[1:-1] - path[:-2])
    v2s = util.unitize(path[1:-1] - path[2:])
    norms = np.cross(np.cross(v1s, v2s), v1s + v2s)
    norms[(norms == 0.0).all(1)] = v1s[(norms == 0.0).all(1)]
    norms = util.unitize(norms)
    final_v1 = util.unitize(path[-1] - path[-2])
    norms = np.vstack((norms, final_v1))
    v1s = np.vstack((v1s, final_v1))

    # Create all side walls by projecting the 3d vertices into each plane
    # in succession
    for i in range(len(norms)):
        verts_3d_prev = verts_3d

        # Rotate if needed
        if angles is not None:
            tf_mat = tf.rotation_matrix(angles[i],
                                        norms[i],
                                        path[i])
            verts_3d_prev = tf.transform_points(verts_3d_prev,
                                                tf_mat)

        # Project vertices onto plane in 3D
        ds = np.einsum('ij,j->i', (path[i + 1] - verts_3d_prev), norms[i])
        ds = ds / np.dot(v1s[i], norms[i])

        verts_3d_new = np.einsum('i,j->ij', ds, v1s[i]) + verts_3d_prev

        # Add to face and vertex lists
        new_faces = [[i + n, (i + 1) % n, i] for i in range(n)]
        new_faces.extend([[(i - 1) % n + n, i + n, i] for i in range(n)])

        # save faces and vertices into a sequence
        faces.append(np.array(new_faces))
        vertices.append(np.vstack((verts_3d, verts_3d_new)))

        verts_3d = verts_3d_new

    # do the main stack operation from a sequence to (n,3) arrays
    # doing one vstack provides a substantial speedup by
    # avoiding a bunch of temporary  allocations
    vertices, faces = util.append_faces(vertices, faces)

    # Create final cap
    x, y, z = util.generate_basis(path[-1] - path[-2])
    vecs = verts_3d - path[-1]
    coords = np.c_[np.einsum('ij,j->i', vecs, x),
                   np.einsum('ij,j->i', vecs, y)]
    base_verts_2d, faces_2d = triangulate_polygon(Polygon(coords), **kwargs)
    base_verts_3d = (np.einsum('i,j->ij', base_verts_2d[:, 0], x) +
                     np.einsum('i,j->ij', base_verts_2d[:, 1], y)) + path[-1]
    faces = np.vstack((faces, faces_2d + len(vertices)))
    vertices = np.vstack((vertices, base_verts_3d))

    return Trimesh(vertices, faces)


def extrude_triangulation(vertices,
                          faces,
                          height,
                          transform=None,
                          **kwargs):
    """
    Extrude a 2D triangulation into a watertight mesh.

    Parameters
    ----------
    vertices : (n, 2) float
      2D vertices
    faces : (m, 3) int
      Triangle indexes of vertices
    height : float
      Distance to extrude triangulation
    **kwargs : dict
      Passed to Trimesh constructor

    Returns
    ---------
    mesh : trimesh.Trimesh
      Mesh created from extrusion
    """
    vertices = np.asanyarray(vertices, dtype=np.float64)
    height = float(height)
    faces = np.asanyarray(faces, dtype=np.int64)

    if not util.is_shape(vertices, (-1, 2)):
        raise ValueError('Vertices must be (n,2)')
    if not util.is_shape(faces, (-1, 3)):
        raise ValueError('Faces must be (n,3)')
    if np.abs(height) < tol.merge:
        raise ValueError('Height must be nonzero!')

    # check the winding of the first few triangles
    signs = np.array([np.cross(*i) for i in
                      np.diff(vertices[faces[:10]], axis=1)])
    # make sure the triangulation is aligned with the sign of
    # the height we've been passed
    if len(signs) > 0 and np.sign(signs.mean()) != np.sign(height):
        faces = np.fliplr(faces)

    # stack the (n,3) faces into (3*n, 2) edges
    edges = faces_to_edges(faces)
    edges_sorted = np.sort(edges, axis=1)
    # edges which only occur once are on the boundary of the polygon
    # since the triangulation may have subdivided the boundary of the
    # shapely polygon, we need to find it again
    edges_unique = grouping.group_rows(
        edges_sorted, require_count=1)

    # (n, 2, 2) set of line segments (positions, not references)
    boundary = vertices[edges[edges_unique]]

    # we are creating two vertical  triangles for every 2D line segment
    # on the boundary of the 2D triangulation
    vertical = np.tile(boundary.reshape((-1, 2)), 2).reshape((-1, 2))
    vertical = np.column_stack((vertical,
                                np.tile([0, height, 0, height],
                                        len(boundary))))
    vertical_faces = np.tile([3, 1, 2, 2, 1, 0],
                             (len(boundary), 1))
    vertical_faces += np.arange(len(boundary)).reshape((-1, 1)) * 4
    vertical_faces = vertical_faces.reshape((-1, 3))

    # stack the (n,2) vertices with zeros to make them (n, 3)
    vertices_3D = util.stack_3D(vertices)

    # a sequence of zero- indexed faces, which will then be appended
    # with offsets to create the final mesh
    faces_seq = [faces[:, ::-1],
                 faces.copy(),
                 vertical_faces]
    vertices_seq = [vertices_3D,
                    vertices_3D.copy() + [0.0, 0, height],
                    vertical]

    # append sequences into flat nicely indexed arrays
    vertices, faces = util.append_faces(vertices_seq, faces_seq)
    if transform is not None:
        # apply transform here to avoid later bookkeeping
        vertices = tf.transform_points(
            vertices, transform)
        # if the transform flips the winding flip faces back
        # so that the normals will be facing outwards
        if tf.flips_winding(transform):
            # fliplr makes arrays non-contiguous
            faces = np.ascontiguousarray(np.fliplr(faces))
    # create mesh object with passed keywords
    mesh = Trimesh(vertices=vertices,
                   faces=faces,
                   **kwargs)
    # only check in strict mode (unit tests)
    if tol.strict:
        assert mesh.volume > 0.0

    return mesh


def triangulate_polygon(polygon,
                        triangle_args=None,
                        engine=None,
                        **kwargs):
    """
    Given a shapely polygon create a triangulation using a
    python interface to `triangle.c` or mapbox-earcut.
    > pip install triangle
    > pip install mapbox_earcut

    Parameters
    ---------
    polygon : Shapely.geometry.Polygon
        Polygon object to be triangulated.
    triangle_args : str or None
        Passed to triangle.triangulate i.e: 'p', 'pq30'
    engine : None or str
      Any value other than 'earcut' will use `triangle`

    Returns
    --------------
    vertices : (n, 2) float
       Points in space
    faces : (n, 3) int
       Index of vertices that make up triangles
    """
    if engine is None or engine == 'earcut':
        # get vertices as sequence where exterior
        # is the first value
        vertices = [np.array(polygon.exterior.coords)]
        vertices.extend(np.array(i.coords)
                        for i in polygon.interiors)
        # record the index from the length of each vertex array
        rings = np.cumsum([len(v) for v in vertices])
        # stack vertices into (n, 2) float array
        vertices = np.vstack(vertices)
        # run triangulation
        faces = _tri_earcut(vertices, rings).reshape(
            (-1, 3)).astype(np.int64).reshape((-1, 3))

        return vertices, faces

    elif engine == 'triangle':
        from triangle import triangulate
        # set default triangulation arguments if not specified
        if triangle_args is None:
            triangle_args = 'p'
            # turn the polygon in to vertices, segments, and holes
        arg = _polygon_to_kwargs(polygon)
        # run the triangulation
        result = triangulate(arg, triangle_args)
        return result['vertices'], result['triangles']
    else:
        log.warning('try running `pip install mapbox-earcut`' +
                    'or explicitly pass:\n' +
                    '`triangulate_polygon(*args, engine="triangle")`\n' +
                    'to use the non-FSF-approved-license triangle engine')
        raise ValueError('no valid triangulation engine!')


def _polygon_to_kwargs(polygon):
    """
    Given a shapely polygon generate the data to pass to
    the triangle mesh generator

    Parameters
    ---------
    polygon : Shapely.geometry.Polygon
      Input geometry

    Returns
    --------
    result : dict
      Has keys: vertices, segments, holes
    """

    if not polygon.is_valid:
        raise ValueError('invalid shapely polygon passed!')

    def round_trip(start, length):
        """
        Given a start index and length, create a series of (n, 2) edges which
        create a closed traversal.

        Examples
        ---------
        start, length = 0, 3
        returns:  [(0,1), (1,2), (2,0)]
        """
        tiled = np.tile(np.arange(start, start + length).reshape((-1, 1)), 2)
        tiled = tiled.reshape(-1)[1:-1].reshape((-1, 2))
        tiled = np.vstack((tiled, [tiled[-1][-1], tiled[0][0]]))
        return tiled

    def add_boundary(boundary, start):
        # coords is an (n, 2) ordered list of points on the polygon boundary
        # the first and last points are the same, and there are no
        # guarantees on points not being duplicated (which will
        # later cause meshpy/triangle to shit a brick)
        coords = np.array(boundary.coords)
        # find indices points which occur only once, and sort them
        # to maintain order
        unique = np.sort(grouping.unique_rows(coords)[0])
        cleaned = coords[unique]

        vertices.append(cleaned)
        facets.append(round_trip(start, len(cleaned)))

        # holes require points inside the region of the hole, which we find
        # by creating a polygon from the cleaned boundary region, and then
        # using a representative point. You could do things like take the mean of
        # the points, but this is more robust (to things like concavity), if
        # slower.
        test = Polygon(cleaned)
        holes.append(np.array(
            test.representative_point().coords)[0])

        return len(cleaned)

    # sequence of (n,2) points in space
    vertices = collections.deque()
    # sequence of (n,2) indices of vertices
    facets = collections.deque()
    # list of (2) vertices in interior of hole regions
    holes = collections.deque()

    start = add_boundary(polygon.exterior, 0)
    for interior in polygon.interiors:
        try:
            start += add_boundary(interior, start)
        except BaseException:
            log.warning('invalid interior, continuing')
            continue

    # create clean (n,2) float array of vertices
    # and (m, 2) int array of facets
    # by stacking the sequence of (p,2) arrays
    vertices = np.vstack(vertices)
    facets = np.vstack(facets).tolist()
    # shapely polygons can include a Z component
    # strip it out for the triangulation
    if vertices.shape[1] == 3:
        vertices = vertices[:, :2]
    result = {'vertices': vertices,
              'segments': facets}
    # holes in meshpy lingo are a (h, 2) list of (x,y) points
    # which are inside the region of the hole
    # we added a hole for the exterior, which we slice away here
    holes = np.array(holes)[1:]
    if len(holes) > 0:
        result['holes'] = holes
    return result


def box(extents=None, transform=None, bounds=None, **kwargs):
    """
    Return a cuboid.

    Parameters
    ------------
    extents : float, or (3,) float
      Edge lengths
    transform: (4, 4) float
      Transformation matrix
    bounds : None or (2, 3) float
      Corners of AABB, overrides extents and transform.
    **kwargs:
        passed to Trimesh to create box

    Returns
    ------------
    geometry : trimesh.Trimesh
      Mesh of a cuboid
    """
    # vertices of the cube
    vertices = np.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1,
                         1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1],
                        order='C',
                        dtype=np.float64).reshape((-1, 3))

    # resize cube based on passed extents
    if bounds is not None:

        if transform is not None or extents is not None:
            raise ValueError('`bounds` overrides `extents`/`transform`!')
        bounds = np.array(bounds, dtype=np.float64)
        if bounds.shape != (2, 3):
            raise ValueError('`bounds` must be (2, 3) float!')
        extents = bounds.ptp(axis=0)
        vertices *= extents
        vertices += bounds[0]
    elif extents is not None:
        extents = np.asanyarray(extents, dtype=np.float64)
        if extents.shape != (3,):
            raise ValueError('Extents must be (3,)!')
        vertices -= 0.5
        vertices *= extents
    else:
        vertices -= 0.5
        extents = np.asarray((1.0, 1.0, 1.0), dtype=np.float64)

    # hardcoded face indices
    faces = [1, 3, 0, 4, 1, 0, 0, 3, 2, 2, 4, 0, 1, 7, 3, 5, 1, 4,
             5, 7, 1, 3, 7, 2, 6, 4, 2, 2, 7, 6, 6, 5, 4, 7, 5, 6]
    faces = np.array(faces, order='C', dtype=np.int64).reshape((-1, 3))

    face_normals = [-1, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, 1, 0, -1,
                    0, 0, 0, 1, 0, 1, 0, 0, 0, -1, 0, 1, 0, 1, 0, 0, 1, 0, 0]
    face_normals = np.asanyarray(face_normals,
                                 order='C',
                                 dtype=np.float64).reshape(-1, 3)

    if 'metadata' not in kwargs:
        kwargs['metadata'] = dict()
    kwargs['metadata'].update(
        {'shape': 'box',
         'extents': extents})

    box = Trimesh(vertices=vertices,
                  faces=faces,
                  face_normals=face_normals,
                  process=False,
                  **kwargs)

    # do the transform here to preserve face normals
    if transform is not None:
        box.apply_transform(transform)

    return box


def icosahedron(**kwargs):
    """
    Create an icosahedron, one of the platonic solids which is has 20 faces.

    Parameters
    ------------
    kwargs : dict
      Passed through to `Trimesh` constructor.

    Returns
    -------------
    ico : trimesh.Trimesh
      Icosahederon centered at the origin.
    """
    t = (1.0 + 5.0**.5) / 2.0
    vertices = [-1, t, 0, 1, t, 0, -1, -t, 0, 1, -t, 0, 0, -1, t, 0, 1, t,
                0, -1, -t, 0, 1, -t, t, 0, -1, t, 0, 1, -t, 0, -1, -t, 0, 1]
    faces = [0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11,
             1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
             3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9,
             4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1]
    # scale vertices so each vertex radius is 1.0
    vertices = np.reshape(vertices, (-1, 3)) / np.sqrt(2.0 + t)
    faces = np.reshape(faces, (-1, 3))

    return Trimesh(vertices=vertices,
                   faces=faces,
                   process=kwargs.pop('process', False),
                   **kwargs)


def icosphere(subdivisions=3, radius=1.0, **kwargs):
    """
    Create an isophere centered at the origin.

    Parameters
    ----------
    subdivisions : int
      How many times to subdivide the mesh.
      Note that the number of faces will grow as function of
      4 ** subdivisions, so you probably want to keep this under ~5
    radius : float
      Desired radius of sphere
    kwargs : dict
      Passed through to `Trimesh` constructor.

    Returns
    ---------
    ico : trimesh.Trimesh
      Meshed sphere
    """

    ico = icosahedron()
    ico._validate = False
    for _ in range(subdivisions):
        ico = ico.subdivide()
        vectors = ico.vertices
        scalar = np.sqrt(np.dot(vectors ** 2, [1, 1, 1]))
        unit = vectors / scalar.reshape((-1, 1))
        ico.vertices += unit * (radius - scalar).reshape((-1, 1))

    if "color" in kwargs:
        warnings.warn(
            '`icosphere(color=...)` is deprecated and will ' +
            'be removed in June 2024: replace with Trimesh constructor ' +
            'kewyword argument `icosphere(face_colors=...)`',
            category=DeprecationWarning, stacklevel=2)
        kwargs["face_colors"] = kwargs.pop("color")

    return Trimesh(vertices=ico.vertices,
                   faces=ico.faces,
                   metadata={'shape': 'sphere',
                             'radius': radius},
                   process=kwargs.pop('process', False),
                   **kwargs)


def uv_sphere(radius=1.0, count=None, transform=None, **kwargs):
    """
    Create a UV sphere (latitude + longitude) centered at the
    origin. Roughly one order of magnitude faster than an
    icosphere but slightly uglier.

    Parameters
    ----------
    radius : float
      Radius of sphere
    count : (2,) int
      Number of latitude and longitude lines
    kwargs : dict
      Passed thgrough
    Returns
    ----------
    mesh : trimesh.Trimesh
       Mesh of UV sphere with specified parameters
    """

    # set the resolution of the uv sphere
    if count is None:
        count = np.array([32, 64], dtype=np.int64)
    else:
        count = np.array(count, dtype=np.int64)
        count += np.mod(count, 2)
        count[1] *= 2

    # generate the 2D curve for the UV sphere
    theta = np.linspace(0.0, np.pi, num=count[0])
    linestring = np.column_stack((np.sin(theta), -np.cos(theta))) * radius

    # revolve the curve to create a volume
    return revolve(linestring=linestring,
                   sections=count[1],
                   transform=transform,
                   metadata={'shape': 'sphere',
                             'radius': radius},
                   **kwargs)


def capsule(height=1.0,
            radius=1.0,
            count=None,
            transform=None):
    """
    Create a mesh of a capsule, or a cylinder with hemispheric ends.

    Parameters
    ----------
    height : float
      Center to center distance of two spheres
    radius : float
      Radius of the cylinder and hemispheres
    count : (2,) int
      Number of sections on latitude and longitude

    Returns
    ----------
    capsule : trimesh.Trimesh
      Capsule geometry with:
        - cylinder axis is along Z
        - one hemisphere is centered at the origin
        - other hemisphere is centered along the Z axis at height
    """
    if count is None:
        count = np.array([32, 64], dtype=np.int64)
    else:
        count = np.array(count, dtype=np.int64)
    count += np.mod(count, 2)

    height = abs(float(height))
    radius = abs(float(radius))

    # create a half circle
    theta = np.linspace(-np.pi / 2.0, np.pi / 2.0, count[0])
    linestring = np.column_stack((np.cos(theta), np.sin(theta))) * radius

    # offset the top and bottom by half the height
    half = len(linestring) // 2
    linestring[:half][:, 1] -= height / 2.0
    linestring[half:][:, 1] += height / 2.0

    return revolve(linestring,
                   sections=count[1],
                   transform=transform,
                   metadata={'shape': 'capsule',
                             'height': height,
                             'radius': radius})


def cone(radius,
         height,
         sections=None,
         transform=None,
         **kwargs):
    """
    Create a mesh of a cone along Z centered at the origin.

    Parameters
    ----------
    radius : float
      The radius of the cylinder
    height : float
      The height of the cylinder
    sections : int or None
      How many pie wedges per revolution
    transform : (4, 4) float or None
      Transform to apply after creation
    **kwargs : dict
      Passed to Trimesh constructor

    Returns
    ----------
    cone: trimesh.Trimesh
      Resulting mesh of a cone
    """
    # create the 2D outline of a cone
    linestring = [[0, 0],
                  [radius, 0],
                  [0, height]]
    # revolve the profile to create a cone
    if 'metadata' not in kwargs:
        kwargs['metadata'] = dict()
    kwargs['metadata'].update(
        {'shape': 'cone',
         'radius': radius,
         'height': height})
    cone = revolve(linestring=linestring,
                   sections=sections,
                   transform=transform,
                   **kwargs)

    return cone


def cylinder(radius,
             height=None,
             sections=None,
             segment=None,
             transform=None,
             **kwargs):
    """
    Create a mesh of a cylinder along Z centered at the origin.

    Parameters
    ----------
    radius : float
      The radius of the cylinder
    height : float or None
      The height of the cylinder
    sections : int or None
      How many pie wedges should the cylinder have
    segment : (2, 3) float
      Endpoints of axis, overrides transform and height
    transform : (4, 4) float
      Transform to apply
    **kwargs:
        passed to Trimesh to create cylinder

    Returns
    ----------
    cylinder: trimesh.Trimesh
      Resulting mesh of a cylinder
    """

    if segment is not None:
        # override transform and height with the segment
        transform, height = _segment_to_cylinder(segment=segment)

    if height is None:
        raise ValueError('either `height` or `segment` must be passed!')

    half = abs(float(height)) / 2.0
    # create a profile to revolve
    linestring = [[0, -half],
                  [radius, -half],
                  [radius, half],
                  [0, half]]
    if 'metadata' not in kwargs:
        kwargs['metadata'] = dict()
    kwargs['metadata'].update(
        {'shape': 'cylinder',
         'height': height,
         'radius': radius})
    # generate cylinder through simple revolution
    return revolve(linestring=linestring,
                   sections=sections,
                   transform=transform,
                   **kwargs)


def annulus(r_min,
            r_max,
            height=None,
            sections=None,
            transform=None,
            segment=None,
            **kwargs):
    """
    Create a mesh of an annular cylinder along Z centered at the origin.

    Parameters
    ----------
    r_min : float
      The inner radius of the annular cylinder
    r_max : float
      The outer radius of the annular cylinder
    height : float
      The height of the annular cylinder
    sections : int or None
      How many pie wedges should the annular cylinder have
    transform : (4, 4) float or None
      Transform to apply to move result from the origin
    segment : None or (2, 3) float
      Override transform and height with a line segment
    **kwargs:
        passed to Trimesh to create annulus

    Returns
    ----------
    annulus : trimesh.Trimesh
      Mesh of annular cylinder
    """
    if segment is not None:
        # override transform and height with the segment if passed
        transform, height = _segment_to_cylinder(segment=segment)

    if height is None:
        raise ValueError('either `height` or `segment` must be passed!')

    r_min = abs(float(r_min))
    # if center radius is zero this is a cylinder
    if r_min < tol.merge:
        return cylinder(radius=r_max,
                        height=height,
                        sections=sections,
                        transform=transform,
                        **kwargs)
    r_max = abs(float(r_max))
    # we're going to center at XY plane so take half the height
    half = abs(float(height)) / 2.0
    # create counter-clockwise rectangle
    linestring = [[r_min, -half],
                  [r_max, -half],
                  [r_max, half],
                  [r_min, half],
                  [r_min, -half]]

    if 'metadata' not in kwargs:
        kwargs['metadata'] = dict()
    kwargs['metadata'].update(
        {'shape': 'annulus',
         'r_min': r_min,
         'r_max': r_max,
         'height': height})

    # revolve the curve
    annulus = revolve(linestring=linestring,
                      sections=sections,
                      transform=transform,
                      **kwargs)

    return annulus


def _segment_to_cylinder(segment):
    """
    Convert a line segment to a transform and height for a cylinder
    or cylinder-like primitive.

    Parameters
    -----------
    segment : (2, 3) float
      3D line segment in space

    Returns
    -----------
    transform : (4, 4) float
      Matrix to move a Z-extruded origin cylinder to segment
    height : float
      The height of the cylinder needed
    """
    segment = np.asanyarray(segment, dtype=np.float64)
    if segment.shape != (2, 3):
        raise ValueError('segment must be 2 3D points!')
    vector = segment[1] - segment[0]
    # override height with segment length
    height = np.linalg.norm(vector)
    # point in middle of line
    midpoint = segment[0] + (vector * 0.5)
    # align Z with our desired direction
    rotation = align_vectors([0, 0, 1], vector)
    # translate to midpoint of segment
    translation = tf.translation_matrix(midpoint)
    # compound the rotation and translation
    transform = np.dot(translation, rotation)
    return transform, height


def random_soup(face_count=100):
    """
    Return random triangles as a Trimesh

    Parameters
    -----------
    face_count : int
      Number of faces desired in mesh

    Returns
    -----------
    soup : trimesh.Trimesh
      Geometry with face_count random faces
    """
    vertices = np.random.random((face_count * 3, 3)) - 0.5
    faces = np.arange(face_count * 3).reshape((-1, 3))
    soup = Trimesh(vertices=vertices, faces=faces)
    return soup


def axis(origin_size=0.04,
         transform=None,
         origin_color=None,
         axis_radius=None,
         axis_length=None):
    """
    Return an XYZ axis marker as a  Trimesh, which represents position
    and orientation. If you set the origin size the other parameters
    will be set relative to it.

    Parameters
    ----------
    transform : (4, 4) float
      Transformation matrix
    origin_size : float
      Radius of sphere that represents the origin
    origin_color : (3,) float or int, uint8 or float
      Color of the origin
    axis_radius : float
      Radius of cylinder that represents x, y, z axis
    axis_length: float
      Length of cylinder that represents x, y, z axis

    Returns
    -------
    marker : trimesh.Trimesh
      Mesh geometry of axis indicators
    """
    # the size of the ball representing the origin
    origin_size = float(origin_size)

    # set the transform and use origin-relative
    # sized for other parameters if not specified
    if transform is None:
        transform = np.eye(4)
    if origin_color is None:
        origin_color = [255, 255, 255, 255]
    if axis_radius is None:
        axis_radius = origin_size / 5.0
    if axis_length is None:
        axis_length = origin_size * 10.0

    # generate a ball for the origin
    axis_origin = uv_sphere(radius=origin_size,
                            count=[10, 10])
    axis_origin.apply_transform(transform)

    # apply color to the origin ball
    axis_origin.visual.face_colors = origin_color

    # create the cylinder for the z-axis
    translation = tf.translation_matrix(
        [0, 0, axis_length / 2])
    z_axis = cylinder(
        radius=axis_radius,
        height=axis_length,
        transform=transform.dot(translation))
    # XYZ->RGB, Z is blue
    z_axis.visual.face_colors = [0, 0, 255]

    # create the cylinder for the y-axis
    translation = tf.translation_matrix(
        [0, 0, axis_length / 2])
    rotation = tf.rotation_matrix(np.radians(-90),
                                  [1, 0, 0])
    y_axis = cylinder(
        radius=axis_radius,
        height=axis_length,
        transform=transform.dot(rotation).dot(translation))
    # XYZ->RGB, Y is green
    y_axis.visual.face_colors = [0, 255, 0]

    # create the cylinder for the x-axis
    translation = tf.translation_matrix(
        [0, 0, axis_length / 2])
    rotation = tf.rotation_matrix(np.radians(90),
                                  [0, 1, 0])
    x_axis = cylinder(
        radius=axis_radius,
        height=axis_length,
        transform=transform.dot(rotation).dot(translation))
    # XYZ->RGB, X is red
    x_axis.visual.face_colors = [255, 0, 0]

    # append the sphere and three cylinders
    marker = util.concatenate([axis_origin,
                               x_axis,
                               y_axis,
                               z_axis])
    return marker


def camera_marker(camera,
                  marker_height=0.4,
                  origin_size=None):
    """
    Create a visual marker for a camera object, including an axis and FOV.

    Parameters
    ---------------
    camera : trimesh.scene.Camera
      Camera object with FOV and transform defined
    marker_height : float
      How far along the camera Z should FOV indicators be
    origin_size : float
      Sphere radius of the origin (default: marker_height / 10.0)

    Returns
    ------------
    meshes : list
      Contains Trimesh and Path3D objects which can be visualized
    """

    # create sane origin size from marker height
    if origin_size is None:
        origin_size = marker_height / 10.0

    # append the visualizations to an array
    meshes = [axis(origin_size=origin_size)]

    try:
        # path is a soft dependency
        from .path.exchange.load import load_path
    except ImportError:
        # they probably don't have shapely installed
        log.warning('unable to create FOV visualization!',
                    exc_info=True)
        return meshes

    # calculate vertices from camera FOV angles
    x = marker_height * np.tan(np.deg2rad(camera.fov[0]) / 2.0)
    y = marker_height * np.tan(np.deg2rad(camera.fov[1]) / 2.0)
    z = marker_height

    # combine the points into the vertices of an FOV visualization
    points = np.array(
        [(0, 0, 0),
         (-x, -y, z),
         (x, -y, z),
         (x, y, z),
         (-x, y, z)],
        dtype=float)

    # create line segments for the FOV visualization
    # a segment from the origin to each bound of the FOV
    segments = np.column_stack(
        (np.zeros_like(points), points)).reshape(
        (-1, 3))

    # add a loop for the outside of the FOV then reshape
    # the whole thing into multiple line segments
    segments = np.vstack((segments,
                          points[[1, 2,
                                  2, 3,
                                  3, 4,
                                  4, 1]])).reshape((-1, 2, 3))

    # add a single Path3D object for all line segments
    meshes.append(load_path(segments))

    return meshes


def truncated_prisms(tris, origin=None, normal=None):
    """
    Return a mesh consisting of multiple watertight prisms below
    a list of triangles, truncated by a specified plane.

    Parameters
    -------------
    triangles : (n, 3, 3) float
      Triangles in space
    origin : None or (3,) float
      Origin of truncation plane
    normal : None or (3,) float
      Unit normal vector of truncation plane

    Returns
    -----------
    mesh : trimesh.Trimesh
      Triangular mesh
    """
    if origin is None:
        transform = np.eye(4)
    else:
        transform = plane_transform(origin=origin, normal=normal)

    # transform the triangles to the specified plane
    transformed = tf.transform_points(
        tris.reshape((-1, 3)), transform).reshape((-1, 9))

    # stack triangles such that every other one is repeated
    vs = np.column_stack((transformed, transformed)).reshape((-1, 3, 3))
    # set the Z value of the second triangle to zero
    vs[1::2, :, 2] = 0
    # reshape triangles to a flat array of points and transform back to
    # original frame
    vertices = tf.transform_points(
        vs.reshape((-1, 3)), matrix=np.linalg.inv(transform))

    # face indexes for a *single* truncated triangular prism
    f = np.array([[2, 1, 0],
                  [3, 4, 5],
                  [0, 1, 4],
                  [1, 2, 5],
                  [2, 0, 3],
                  [4, 3, 0],
                  [5, 4, 1],
                  [3, 5, 2]])
    # find the projection of each triangle with the normal vector
    cross = np.dot([0, 0, 1], triangles.cross(
        transformed.reshape((-1, 3, 3))).T)
    # stack faces into one prism per triangle
    f_seq = np.tile(f, (len(transformed), 1)).reshape((-1, len(f), 3))
    # if the normal of the triangle was positive flip the winding
    f_seq[cross > 0] = np.fliplr(f)
    # offset stacked faces to create correct indices
    faces = (f_seq + (np.arange(len(f_seq)) *
             6).reshape((-1, 1, 1))).reshape((-1, 3))

    # create a mesh from the data
    mesh = Trimesh(vertices=vertices, faces=faces, process=False)

    return mesh


# trimesh/curvature.py

"""
curvature.py
---------------

Query mesh curvature.
"""
import numpy as np

from . import util

try:
    from scipy.sparse import coo_matrix
except ImportError as E:
    from . import exceptions
    coo_matrix = exceptions.ExceptionWrapper(E)


def face_angles_sparse(mesh):
    """
    A sparse matrix representation of the face angles.

    Returns
    ----------
    sparse : scipy.sparse.coo_matrix
      matrix is float shaped (len(vertices), len(faces))
    """
    matrix = coo_matrix((
        mesh.face_angles.flatten(),
        (mesh.faces_sparse.row, mesh.faces_sparse.col)),
        mesh.faces_sparse.shape)
    return matrix


def vertex_defects(mesh):
    """
    Return the vertex defects, or (2*pi) minus the sum of the
    angles of every face that includes that vertex.

    If a vertex is only included by coplanar triangles, this
    will be zero. For convex regions this is positive, and
    concave negative.

    Returns
    --------
    vertex_defect : (len(self.vertices), ) float
                     Vertex defect at the every vertex
    """
    angle_sum = np.array(mesh.face_angles_sparse.sum(axis=1)).flatten()
    defect = (2 * np.pi) - angle_sum
    return defect


def discrete_gaussian_curvature_measure(mesh, points, radius):
    """
    Return the discrete gaussian curvature measure of a sphere
    centered at a point as detailed in 'Restricted Delaunay
    triangulations and normal cycle'- Cohen-Steiner and Morvan.

    This is the sum of the vertex defects at all vertices
    within the radius for each point.

    Parameters
    ----------
    points : (n, 3) float
      Points in space
    radius : float ,
      The sphere radius, which can be zero if vertices
      passed are points.

    Returns
    --------
    gaussian_curvature:  (n,) float
      Discrete gaussian curvature measure.
    """

    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    nearest = mesh.kdtree.query_ball_point(points, radius)
    gauss_curv = [mesh.vertex_defects[vertices].sum() for vertices in nearest]

    return np.asarray(gauss_curv)


def discrete_mean_curvature_measure(mesh, points, radius):
    """
    Return the discrete mean curvature measure of a sphere
    centered at a point as detailed in 'Restricted Delaunay
    triangulations and normal cycle'- Cohen-Steiner and Morvan.

    This is the sum of the angle at all edges contained in the
    sphere for each point.

    Parameters
    ----------
    points : (n, 3) float
      Points in space
    radius : float
      Sphere radius which should typically be greater than zero

    Returns
    --------
    mean_curvature : (n,) float
      Discrete mean curvature measure.
    """

    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    # axis aligned bounds
    bounds = np.column_stack((points - radius,
                              points + radius))

    # line segments that intersect axis aligned bounding box
    candidates = [list(mesh.face_adjacency_tree.intersection(b))
                  for b in bounds]

    mean_curv = np.empty(len(points))
    for i, (x, x_candidates) in enumerate(zip(points, candidates)):
        endpoints = mesh.vertices[mesh.face_adjacency_edges[x_candidates]]
        lengths = line_ball_intersection(
            endpoints[:, 0],
            endpoints[:, 1],
            center=x,
            radius=radius)
        angles = mesh.face_adjacency_angles[x_candidates]
        signs = np.where(mesh.face_adjacency_convex[x_candidates], 1, -1)
        mean_curv[i] = (lengths * angles * signs).sum() / 2

    return mean_curv


def line_ball_intersection(start_points, end_points, center, radius):
    """
    Compute the length of the intersection of a line segment with a ball.

    Parameters
    ----------
    start_points : (n,3) float, list of points in space
    end_points   : (n,3) float, list of points in space
    center       : (3,) float, the sphere center
    radius       : float, the sphere radius

    Returns
    --------
    lengths: (n,) float, the lengths.

    """

    # We solve for the intersection of |x-c|**2 = r**2 and
    # x = o + dL. This yields
    # d = (-l.(o-c) +- sqrt[ l.(o-c)**2 - l.l((o-c).(o-c) - r^**2) ]) / l.l
    L = end_points - start_points
    oc = start_points - center  # o-c
    r = radius
    ldotl = np.einsum('ij, ij->i', L, L)  # l.l
    ldotoc = np.einsum('ij, ij->i', L, oc)  # l.(o-c)
    ocdotoc = np.einsum('ij, ij->i', oc, oc)  # (o-c).(o-c)
    discrims = ldotoc**2 - ldotl * (ocdotoc - r**2)

    # If discriminant is non-positive, then we have zero length
    lengths = np.zeros(len(start_points))
    # Otherwise we solve for the solns with d2 > d1.
    m = discrims > 0  # mask
    d1 = (-ldotoc[m] - np.sqrt(discrims[m])) / ldotl[m]
    d2 = (-ldotoc[m] + np.sqrt(discrims[m])) / ldotl[m]

    # Line segment means we have 0 <= d <= 1
    d1 = np.clip(d1, 0, 1)
    d2 = np.clip(d2, 0, 1)

    # Length is |o + d2 l - o + d1 l|  = (d2 - d1) |l|
    lengths[m] = (d2 - d1) * np.sqrt(ldotl[m])

    return lengths


def sphere_ball_intersection(R, r):
    """
    Compute the surface area of the intersection of sphere of radius R centered
    at (0, 0, 0) with a ball of radius r centered at (R, 0, 0).

    Parameters
    ----------
    R : float, sphere radius
    r : float, ball radius

    Returns
    --------
    area: float, the surface are.
    """
    x = (2 * R**2 - r**2) / (2 * R)  # x coord of plane
    if x >= -R:
        return 2 * np.pi * R * (R - x)
    if x < -R:
        return 4 * np.pi * R**2


# trimesh/decomposition.py

from . import interfaces


def convex_decomposition(mesh, **kwargs):
    """
    Compute an approximate convex decomposition of a mesh.

    Parameters
    ----------
    mesh : trimesh.Trimesh
      Mesh to be decomposed into convex parts

    Returns
    -------
    mesh_args : list
      List of **kwargs for Trimeshes that are nearly
      convex and approximate the original.
    """
    # decompositions require testVHACD
    if interfaces.vhacd.exists:
        return interfaces.vhacd.convex_decomposition(mesh, **kwargs)
    else:
        raise ValueError('convex compositions require testVHACD installed!')


# trimesh/exceptions.py

"""
exceptions.py
----------------

Wrap exceptions.
"""


class ExceptionWrapper(object):
    """
    Create a dummy object which will raise an exception when attributes
    are accessed (i.e. when used as a module) or when called (i.e.
    when used like a function)

    For soft dependencies we want to survive failing to import but
    we would like to raise an appropriate error when the functionality is
    actually requested so the user gets an easily debuggable message.
    """

    def __init__(self, exception):
        self.exception = exception

    def __getattribute__(self, *args, **kwargs):
        # will raise when this object is accessed like an object
        # if it's asking for our class type return None
        # this allows isinstance() checks to not re-raise
        if args[0] == '__class__':
            return None.__class__
        # otherwise raise our original exception
        raise super(ExceptionWrapper, self).__getattribute__('exception')

    def __call__(self, *args, **kwargs):
        # will raise when this object is called like a function
        raise super(ExceptionWrapper, self).__getattribute__('exception')


# trimesh/geometry.py

import numpy as np

from . import util
from .constants import log

try:
    import scipy.sparse
except BaseException as E:
    from . import exceptions
    # raise E again if anyone tries to use sparse
    scipy = exceptions.ExceptionWrapper(E)


def plane_transform(origin, normal):
    """
    Given the origin and normal of a plane find the transform
    that will move that plane to be coplanar with the XY plane.

    Parameters
    ----------
    origin : (3,) float
        Point that lies on the plane
    normal : (3,) float
        Vector that points along normal of plane

    Returns
    ---------
    transform: (4,4) float
        Transformation matrix to move points onto XY plane
    """
    transform = align_vectors(normal, [0, 0, 1])
    if origin is not None:
        transform[:3, 3] = -np.dot(
            transform, np.append(origin, 1))[:3]
    return transform


def align_vectors(a, b, return_angle=False):
    """
    Find the rotation matrix that transforms one 3D vector
    to another.

    Parameters
    ------------
    a : (3,) float
      Unit vector
    b : (3,) float
      Unit vector
    return_angle : bool
      Return the angle between vectors or not

    Returns
    -------------
    matrix : (4, 4) float
      Homogeneous transform to rotate from `a` to `b`
    angle : float
      If `return_angle` angle in radians between `a` and `b`

    """
    a = np.array(a, dtype=np.float64)
    b = np.array(b, dtype=np.float64)
    if a.shape != (3,) or b.shape != (3,):
        raise ValueError('vectors must be (3,)!')

    # find the SVD of the two vectors
    au = np.linalg.svd(a.reshape((-1, 1)))[0]
    bu = np.linalg.svd(b.reshape((-1, 1)))[0]

    if np.linalg.det(au) < 0:
        au[:, -1] *= -1.0
    if np.linalg.det(bu) < 0:
        bu[:, -1] *= -1.0

    # put rotation into homogeneous transformation
    matrix = np.eye(4)
    matrix[:3, :3] = bu.dot(au.T)

    if return_angle:
        # projection of a onto b
        # first row of SVD result is normalized source vector
        dot = np.dot(au[0], bu[0])
        # clip to avoid floating point error
        angle = np.arccos(np.clip(dot, -1.0, 1.0))
        if dot < -1e-5:
            angle += np.pi
        return matrix, angle

    return matrix


def faces_to_edges(faces, return_index=False):
    """
    Given a list of faces (n,3), return a list of edges (n*3,2)

    Parameters
    -----------
    faces : (n, 3) int
      Vertex indices representing faces

    Returns
    -----------
    edges : (n*3, 2) int
      Vertex indices representing edges
    """
    faces = np.asanyarray(faces)

    # each face has three edges
    edges = faces[:, [0, 1, 1, 2, 2, 0]].reshape((-1, 2))

    if return_index:
        # edges are in order of faces due to reshape
        face_index = np.tile(np.arange(len(faces)),
                             (3, 1)).T.reshape(-1)
        return edges, face_index
    return edges


def vector_angle(pairs):
    """
    Find the angles between pairs of unit vectors.

    Parameters
    ----------
    pairs : (n, 2, 3) float
      Unit vector pairs

    Returns
    ----------
    angles : (n,) float
      Angles between vectors in radians
    """
    pairs = np.asanyarray(pairs, dtype=np.float64)
    if len(pairs) == 0:
        return np.array([])
    elif util.is_shape(pairs, (2, 3)):
        pairs = pairs.reshape((-1, 2, 3))
    elif not util.is_shape(pairs, (-1, 2, (2, 3))):
        raise ValueError('pairs must be (n,2,(2|3))!')

    # do the dot product between vectors
    dots = util.diagonal_dot(pairs[:, 0], pairs[:, 1])
    # clip for floating point error
    dots = np.clip(dots, -1.0, 1.0)
    # do cos and remove arbitrary sign
    angles = np.abs(np.arccos(dots))

    return angles


def triangulate_quads(quads, dtype=np.int64):
    """
    Given an array of quad faces return them as triangle faces,
    also handles pure triangles and mixed triangles and quads.

    Parameters
    -----------
    quads: (n, 4) int
      Vertex indices of quad faces.

    Returns
    -----------
    faces : (m, 3) int
      Vertex indices of triangular faces.c
    """

    if len(quads) == 0:
        return np.zeros(0, dtype=dtype)

    try:
        # this will fail in newer versions of numpy
        # if there are mixed quads and tris
        quads = np.array(quads, dtype=dtype)

        if len(quads.shape) == 2 and quads.shape[1] == 3:
            # if they are just triangles return immediately
            return quads.astype(dtype)

        if len(quads.shape) == 2 and quads.shape[1] == 4:
            # if they are just quads stack and return
            return np.vstack((quads[:, [0, 1, 2]],
                              quads[:, [2, 3, 0]])).astype(dtype)
    except ValueError:
        # new numpy raises an error for sequences
        pass

    # we made it here so we have mixed tris/quads/polygons
    # filter into the three cases
    tri = np.array([i for i in quads if len(i) == 3])
    quad = np.array([i for i in quads if len(i) == 4])
    # triangulate arbitrary polygons as triangle fans
    # this isn't guaranteed to be sane if the polygons
    # aren't convex but that would require a real maniac
    poly = [[[f[0], f[i + 1], f[i + 2]]
             for i in range(len(f) - 2)]
            for f in quads if len(f) > 4]

    if len(quad) == 0 and len(poly) == 0:
        return tri.astype(dtype)
    if len(poly) > 0:
        poly = np.vstack(poly)
    if len(quad) > 0:
        quad = np.vstack((quad[:, [0, 1, 2]],
                          quad[:, [2, 3, 0]]))
    # combine triangulated quads with triangles
    return util.vstack_empty([
        tri, quad, poly]).astype(dtype)


def vertex_face_indices(vertex_count,
                        faces,
                        faces_sparse):
    """
    Find vertex face indices from the faces array of vertices

    Parameters
    -----------
    vertex_count : int
      The number of vertices faces refer to
    faces : (n, 3) int
      List of vertex indices
    faces_sparse : scipy.sparse.COO
      Sparse matrix

    Returns
    -----------
    vertex_faces : (vertex_count, ) int
      Face indices for every vertex
      Array padded with -1 in each row for all vertices with fewer
      face indices than the max number of face indices.
    """
    # Create 2D array with row for each vertex and
    # length of max number of faces for a vertex
    try:
        counts = np.bincount(
            faces.flatten(), minlength=vertex_count)
    except TypeError:
        # casting failed on 32 bit Windows
        log.warning('casting failed, falling back!')
        # fall back to np.unique (usually ~35x slower than bincount)
        counts = np.unique(faces.flatten(), return_counts=True)[1]
    assert len(counts) == vertex_count
    assert faces.max() < vertex_count

    # start cumulative sum at zero and clip off the last value
    starts = np.append(0, np.cumsum(counts)[:-1])
    # pack incrementing array into final shape
    pack = np.arange(counts.max()) + starts[:, None]
    # pad each row with -1 to pad to the max length
    padded = -(pack >= (starts + counts)[:, None]).astype(np.int64)

    try:
        # do most of the work with a sparse dot product
        identity = scipy.sparse.identity(len(faces), dtype=int)
        sorted_faces = faces_sparse.dot(identity).nonzero()[1]
        # this will fail if any face was degenerate
        # TODO
        # figure out how to filter out degenerate faces from sparse
        # result if sorted_faces.size != faces.size
        padded[padded == 0] = sorted_faces
    except BaseException:
        # fall back to a slow loop
        log.warning('vertex_faces falling back to slow loop! ' +
                    'mesh probably has degenerate faces',
                    exc_info=True)
        sort = np.zeros(faces.size, dtype=np.int64)
        flat = faces.flatten()
        for v in range(vertex_count):
            # assign the data in order
            sort[starts[v]:starts[v] + counts[v]
                 ] = (np.where(flat == v)[0] // 3)[::-1]
        padded[padded == 0] = sort
    return padded


def mean_vertex_normals(vertex_count,
                        faces,
                        face_normals,
                        sparse=None,
                        **kwargs):
    """
    Find vertex normals from the mean of the faces that contain
    that vertex.

    Parameters
    -----------
    vertex_count : int
      The number of vertices faces refer to
    faces : (n, 3) int
      List of vertex indices
    face_normals : (n, 3) float
      Normal vector for each face

    Returns
    -----------
    vertex_normals : (vertex_count, 3) float
      Normals for every vertex
      Vertices unreferenced by faces will be zero.
    """
    def summed_sparse():
        # use a sparse matrix of which face contains each vertex to
        # figure out the summed normal at each vertex
        # allow cached sparse matrix to be passed
        if sparse is None:
            matrix = index_sparse(vertex_count, faces)
        else:
            matrix = sparse
        summed = matrix.dot(face_normals)
        return summed

    def summed_loop():
        # loop through every face, in tests was ~50x slower than
        # doing this with a sparse matrix
        summed = np.zeros((vertex_count, 3))
        for face, normal in zip(faces, face_normals):
            summed[face] += normal
        return summed

    try:
        summed = summed_sparse()
    except BaseException:
        log.warning(
            'unable to use sparse matrix, falling back!',
            exc_info=True)
        summed = summed_loop()

    # invalid normals will be returned as zero
    vertex_normals = util.unitize(summed)

    return vertex_normals


def weighted_vertex_normals(vertex_count,
                            faces,
                            face_normals,
                            face_angles,
                            use_loop=False):
    """
    Compute vertex normals from the faces that contain that vertex.
    The contibution of a face's normal to a vertex normal is the
    ratio of the corner-angle in which the vertex is, with respect
    to the sum of all corner-angles surrounding the vertex.

    Grit Thuerrner & Charles A. Wuethrich (1998)
    Computing Vertex Normals from Polygonal Facets,
    Journal of Graphics Tools, 3:1, 43-46

    Parameters
    -----------
    vertex_count : int
      The number of vertices faces refer to
    faces : (n, 3) int
      List of vertex indices
    face_normals : (n, 3) float
      Normal vector for each face
    face_angles : (n, 3) float
      Angles at each vertex in the face

    Returns
    -----------
    vertex_normals : (vertex_count, 3) float
      Normals for every vertex
      Vertices unreferenced by faces will be zero.
    """
    def summed_sparse():
        # use a sparse matrix of which face contains each vertex to
        # figure out the summed normal at each vertex
        # allow cached sparse matrix to be passed
        # fill the matrix with vertex-corner angles as weights
        matrix = index_sparse(vertex_count, faces, data=face_angles.ravel())
        return matrix.dot(face_normals)

    def summed_loop():
        summed = np.zeros((vertex_count, 3), np.float64)
        for vertex_idx in np.arange(vertex_count):
            # loop over all vertices
            # compute normal contributions from surrounding faces
            # obviously slower than with the sparse matrix
            face_idxs, inface_idxs = np.where(faces == vertex_idx)
            surrounding_angles = face_angles[face_idxs, inface_idxs]
            summed[vertex_idx] = np.dot(
                surrounding_angles /
                surrounding_angles.sum(),
                face_normals[face_idxs])

        return summed

    # normals should be unit vectors
    face_ok = (face_normals ** 2).sum(axis=1) > 0.5
    # don't consider faces with invalid normals
    faces = faces[face_ok]
    face_normals = face_normals[face_ok]
    face_angles = face_angles[face_ok]

    if not use_loop:
        try:
            return util.unitize(summed_sparse())
        except BaseException:
            log.warning(
                'unable to use sparse matrix, falling back!',
                exc_info=True)
    # we either crashed or were asked to loop
    return util.unitize(summed_loop())


def index_sparse(columns, indices, data=None, dtype=None):
    """
    Return a sparse matrix for which vertices are contained in which faces.
    A data vector can be passed which is then used instead of booleans

    Parameters
    ------------
    columns : int
      Number of columns, usually number of vertices
    indices : (m, d) int
      Usually mesh.faces

    Returns
    ---------
    sparse: scipy.sparse.coo_matrix of shape (columns, len(faces))
            dtype is boolean

    Examples
     ----------
    In [1]: sparse = faces_sparse(len(mesh.vertices), mesh.faces)

    In [2]: sparse.shape
    Out[2]: (12, 20)

    In [3]: mesh.faces.shape
    Out[3]: (20, 3)
co
    In [4]: mesh.vertices.shape
    Out[4]: (12, 3)

    In [5]: dense = sparse.toarray().astype(int)

    In [6]: dense
    Out[6]:
    array([[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
           [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
           [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
           [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
           [0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
           [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0],
           [0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
           [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1],
           [1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0],
           [0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0],
           [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1],
           [0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1]])

    In [7]: dense.sum(axis=0)
    Out[7]: array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
    """
    indices = np.asanyarray(indices)
    columns = int(columns)

    # flattened list
    row = indices.reshape(-1)
    col = np.tile(np.arange(len(indices)).reshape(
        (-1, 1)), (1, indices.shape[1])).reshape(-1)

    shape = (columns, len(indices))
    if data is None:
        data = np.ones(len(col), dtype=bool)
    elif len(data) != len(col):
        raise ValueError('data incorrect length')

    if dtype is not None:
        data = data.astype(dtype)

    # assemble into sparse matrix
    matrix = scipy.sparse.coo_matrix((data, (row, col)),
                                     shape=shape,
                                     dtype=data.dtype)

    return matrix


# trimesh/graph.py

"""
graph.py
-------------

Deal with graph operations. Primarily deal with graphs in (n, 2)
edge list form, and abstract the backend graph library being used.

Currently uses networkx or scipy.sparse.csgraph backend.
"""

import numpy as np
import collections

from . import util
from . import grouping
from . import exceptions

from .constants import log, tol
from .geometry import faces_to_edges

try:
    from scipy.sparse import csgraph, coo_matrix
except BaseException as E:
    # re-raise exception when used
    csgraph = exceptions.ExceptionWrapper(E)
    coo_matrix = exceptions.ExceptionWrapper(E)

try:
    import networkx as nx
except BaseException as E:
    # create a dummy module which will raise the ImportError
    # or other exception only when someone tries to use networkx
    nx = exceptions.ExceptionWrapper(E)


def face_adjacency(faces=None,
                   mesh=None,
                   return_edges=False):
    """
    Returns an (n, 2) list of face indices.
    Each pair of faces in the list shares an edge, making them adjacent.


    Parameters
    -----------
    faces : (n, 3) int, or None
        Vertex indices representing triangles
    mesh : Trimesh object
        If passed will used cached edges
        instead of generating from faces
    return_edges : bool
        Return the edges shared by adjacent faces

    Returns
    ----------
    adjacency : (m, 2) int
        Indexes of faces that are adjacent
    edges: (m, 2) int
        Only returned if return_edges is True
        Indexes of vertices which make up the
        edges shared by the adjacent faces

    Examples
    ----------
    This is useful for lots of things such as finding
    face- connected components:
    ```python
    >>> graph = nx.Graph()
    >>> graph.add_edges_from(mesh.face_adjacency)
    >>> groups = nx.connected_components(graph_connected)
    ```
    """

    if mesh is None:
        # first generate the list of edges for the current faces
        # also return the index for which face the edge is from
        edges, edges_face = faces_to_edges(faces, return_index=True)
        # make sure edge rows are sorted
        edges.sort(axis=1)
    else:
        # if passed a mesh, used the cached values
        edges = mesh.edges_sorted
        edges_face = mesh.edges_face

    # this will return the indices for duplicate edges
    # every edge appears twice in a well constructed mesh
    # so for every row in edge_idx:
    # edges[edge_idx[*][0]] == edges[edge_idx[*][1]]
    # in this call to group rows we discard edges which
    # don't occur twice
    edge_groups = grouping.group_rows(edges, require_count=2)

    if len(edge_groups) == 0:
        log.debug('No adjacent faces detected! Did you merge vertices?')

    # the pairs of all adjacent faces
    # so for every row in face_idx, self.faces[face_idx[*][0]] and
    # self.faces[face_idx[*][1]] will share an edge
    adjacency = edges_face[edge_groups]

    # degenerate faces may appear in adjacency as the same value
    nondegenerate = adjacency[:, 0] != adjacency[:, 1]
    adjacency = adjacency[nondegenerate]

    # sort pairs in-place so we can search for indexes with ordered pairs
    adjacency.sort(axis=1)

    if return_edges:
        adjacency_edges = edges[edge_groups[:, 0][nondegenerate]]
        assert len(adjacency_edges) == len(adjacency)
        return adjacency, adjacency_edges
    return adjacency


def face_neighborhood(mesh):
    """
    Find faces that share a vertex i.e. 'neighbors' faces.
    Relies on the fact that an adjacency matrix at a power p
    contains the number of paths of length p connecting two nodes.
    Here we take the bipartite graph from mesh.faces_sparse to the power 2.
    The non-zeros are the faces connected by one vertex.

    Returns
    ----------
    neighborhood : (n, 2) int
        Pairs of faces which share a vertex
    """

    VT = mesh.faces_sparse
    TT = VT.T * VT
    TT.setdiag(0)
    TT.eliminate_zeros()
    TT = TT.tocoo()
    neighborhood = np.concatenate((TT.row[:, None], TT.col[:, None]), axis=-1)
    return neighborhood


def face_adjacency_unshared(mesh):
    """
    Return the vertex index of the two vertices not in the shared
    edge between two adjacent faces

    Parameters
    ----------
    mesh : Trimesh object
      Input mesh

    Returns
    -----------
    vid_unshared : (len(mesh.face_adjacency), 2) int
      Indexes of mesh.vertices
      for degenerate faces without exactly
      one unshared vertex per face it will be -1
    """

    # the non- shared vertex index is the same shape
    # as face_adjacency holding vertex indices vs face indices
    vid_unshared = np.zeros_like(mesh.face_adjacency,
                                 dtype=np.int64) - 1
    # get the shared edges between adjacent faces
    edges = mesh.face_adjacency_edges

    # loop through the two columns of face adjacency
    for i, fid in enumerate(mesh.face_adjacency.T):
        # faces from the current column of face adjacency
        faces = mesh.faces[fid]
        # should have one True per row of (3,)
        # index of vertex not included in shared edge
        unshared = np.logical_not(np.logical_or(
            faces == edges[:, 0].reshape((-1, 1)),
            faces == edges[:, 1].reshape((-1, 1))))
        # each row should have exactly one uncontained verted
        row_ok = unshared.sum(axis=1) == 1
        # any degenerate row should be ignored
        unshared[~row_ok, :] = False
        # set the
        vid_unshared[row_ok, i] = faces[unshared]

    return vid_unshared


def face_adjacency_radius(mesh):
    """
    Compute an approximate radius between adjacent faces.

    Parameters
    --------------
    mesh : trimesh.Trimesh

    Returns
    -------------
    radii : (len(self.face_adjacency),) float
      Approximate radius between faces
      Parallel faces will have a value of np.inf
    span :  (len(self.face_adjacency),) float
      Perpendicular projection distance of two
      unshared vertices onto the shared edge
    """

    # solve for the radius of the adjacent faces
    #       distance
    # R = ---------------
    #     2 * sin(theta)
    nonzero = mesh.face_adjacency_angles > np.radians(.01)
    denominator = np.abs(
        2.0 * np.sin(mesh.face_adjacency_angles[nonzero]))

    # consider the distance between the non- shared vertices of the
    # face adjacency pair as the key distance
    point_pairs = mesh.vertices[mesh.face_adjacency_unshared]
    vectors = np.diff(point_pairs,
                      axis=1).reshape((-1, 3))

    # the vertex indices of the shared edge for the adjacency pairx
    edges = mesh.face_adjacency_edges
    # unit vector along shared the edge
    edges_vec = util.unitize(np.diff(mesh.vertices[edges],
                                     axis=1).reshape((-1, 3)))

    # the vector of the perpendicular projection to the shared edge
    perp = np.subtract(
        vectors, (util.diagonal_dot(
            vectors, edges_vec).reshape(
            (-1, 1)) * edges_vec))
    # the length of the perpendicular projection
    span = util.row_norm(perp)

    # complete the values for non- infinite radii
    radii = np.ones(len(mesh.face_adjacency)) * np.inf
    radii[nonzero] = span[nonzero] / denominator

    return radii, span


def vertex_adjacency_graph(mesh):
    """
    Returns a networkx graph representing the vertices and
    their connections in the mesh.

    Parameters
    ----------
    mesh : Trimesh object

    Returns
    ---------
    graph : networkx.Graph
        Graph representing vertices and edges between
        them where vertices are nodes and edges are edges

    Examples
    ----------
    This is useful for getting nearby vertices for a given vertex,
    potentially for some simple smoothing techniques.
    >>> graph = mesh.vertex_adjacency_graph
    >>> graph.neighbors(0)
    > [1, 3, 4]
    """
    g = nx.Graph()
    g.add_edges_from(mesh.edges_unique)
    return g


def shared_edges(faces_a, faces_b):
    """
    Given two sets of faces, find the edges which are in both sets.

    Parameters
    ---------
    faces_a : (n, 3) int
      Array of faces
    faces_b : (m, 3) int
      Array of faces

    Returns
    ---------
    shared : (p, 2) int
      Edges shared between faces
    """
    e_a = np.sort(faces_to_edges(faces_a), axis=1)
    e_b = np.sort(faces_to_edges(faces_b), axis=1)
    shared = grouping.boolean_rows(
        e_a, e_b, operation=np.intersect1d)
    return shared


def facets(mesh, engine=None):
    """
    Find the list of parallel adjacent faces.

    Parameters
    -----------
    mesh :  trimesh.Trimesh
    engine : str
       Which graph engine to use:
       ('scipy', 'networkx')

    Returns
    ---------
    facets : sequence of (n,) int
        Groups of face indexes of
        parallel adjacent faces.
    """
    # what is the radius of a circle that passes through the perpendicular
    # projection of the vector between the two non- shared vertices
    # onto the shared edge, with the face normal from the two adjacent faces
    radii = mesh.face_adjacency_radius
    # what is the span perpendicular to the shared edge
    span = mesh.face_adjacency_span
    # a very arbitrary formula for declaring two adjacent faces
    # parallel in a way that is hopefully (and anecdotally) robust
    # to numeric error
    # a common failure mode is two faces that are very narrow with a slight
    # angle between them, so here we divide by the perpendicular span
    # to penalize very narrow faces, and then square it just for fun
    parallel = np.ones(len(radii), dtype=bool)
    # if span is zero we know faces are small/parallel
    nonzero = np.abs(span) > tol.zero
    # faces with a radii/span ratio larger than a threshold pass
    parallel[nonzero] = (radii[nonzero] /
                         span[nonzero]) ** 2 > tol.facet_threshold

    # run connected components on the parallel faces to group them
    components = connected_components(
        mesh.face_adjacency[parallel],
        nodes=np.arange(len(mesh.faces)),
        min_len=2,
        engine=engine)

    return components


def split(mesh, only_watertight=True, adjacency=None, engine=None, **kwargs):
    """
    Split a mesh into multiple meshes from face
    connectivity.

    If only_watertight is true it will only return
    watertight meshes and will attempt to repair
    single triangle or quad holes.

    Parameters
    ----------
    mesh : trimesh.Trimesh
    only_watertight: bool
      Only return watertight components
    adjacency : (n, 2) int
      Face adjacency to override full mesh
    engine : str or None
      Which graph engine to use

    Returns
    ----------
    meshes : (m,) trimesh.Trimesh
      Results of splitting
    """
    if adjacency is None:
        adjacency = mesh.face_adjacency

    # if only watertight the shortest thing we can split has 3 triangles
    if only_watertight:
        min_len = 4
    else:
        min_len = 1

    components = connected_components(
        edges=adjacency,
        nodes=np.arange(len(mesh.faces)),
        min_len=min_len,
        engine=engine)
    meshes = mesh.submesh(
        components, only_watertight=only_watertight, **kwargs)
    return meshes


def connected_components(edges,
                         min_len=1,
                         nodes=None,
                         engine=None):
    """
    Find groups of connected nodes from an edge list.

    Parameters
    -----------
    edges : (n, 2) int
      Edges between nodes
    nodes : (m, ) int or None
      List of nodes that exist
    min_len : int
      Minimum length of a component group to return
    engine :  str or None
      Which graph engine to use (None for automatic):
      (None, 'networkx', 'scipy')


    Returns
    -----------
    components : (n,) sequence of (*,) int
      Nodes which are connected
    """
    def components_networkx():
        """
        Find connected components using networkx
        """
        graph = nx.from_edgelist(edges)
        # make sure every face has a node, so single triangles
        # aren't discarded (as they aren't adjacent to anything)
        if min_len <= 1:
            graph.add_nodes_from(nodes)
        return [list(i) for i in nx.connected_components(graph)]

    def components_csgraph():
        """
        Find connected components using scipy.sparse.csgraph
        """
        # label each node
        labels = connected_component_labels(edges,
                                            node_count=node_count)

        # we have to remove results that contain nodes outside
        # of the specified node set and reindex
        contained = np.zeros(node_count, dtype=bool)
        contained[nodes] = True
        index = np.arange(node_count, dtype=np.int64)[contained]
        components = grouping.group(labels[contained], min_len=min_len)
        return [index[c] for c in components]

        return components

    # check input edges
    edges = np.asanyarray(edges, dtype=np.int64)
    # if no nodes were specified just use unique
    if nodes is None:
        nodes = np.unique(edges)

    # exit early if we have no nodes
    if len(nodes) == 0:
        return []
    elif len(edges) == 0:
        if min_len <= 1:
            return np.reshape(nodes, (-1, 1)).tolist()
        else:
            return []

    if not util.is_shape(edges, (-1, 2)):
        raise ValueError('edges must be (n, 2)!')

    # find the maximum index referenced in either nodes or edges
    counts = [0]
    if len(edges) > 0:
        counts.append(edges.max())
    if len(nodes) > 0:
        counts.append(nodes.max())
    node_count = np.max(counts) + 1

    # remove edges that don't have both nodes in the node set
    mask = np.zeros(node_count, dtype=bool)
    mask[nodes] = True
    edges_ok = mask[edges].all(axis=1)
    edges = edges[edges_ok]

    # networkx is pure python and is usually 5-10x slower than scipy
    engines = collections.OrderedDict((
        ('scipy', components_csgraph),
        ('networkx', components_networkx)))

    # if a graph engine has explicitly been requested use it
    if engine in engines:
        return engines[engine]()

    # otherwise, go through our ordered list of graph engines
    # until we get to one that has actually been installed
    for function in engines.values():
        try:
            return function()
        # will be raised if the library didn't import correctly above
        except BaseException:
            continue
    raise ImportError('no graph engines available!')


def connected_component_labels(edges, node_count=None):
    """
    Label graph nodes from an edge list, using scipy.sparse.csgraph

    Parameters
    -----------
    edges : (n, 2) int
       Edges of a graph
    node_count : int, or None
        The largest node in the graph.

    Returns
    ----------
    labels : (node_count,) int
        Component labels for each node
    """
    matrix = edges_to_coo(edges, node_count)
    body_count, labels = csgraph.connected_components(
        matrix, directed=False)

    if node_count is not None:
        assert len(labels) == node_count

    return labels


def split_traversal(traversal,
                    edges,
                    edges_hash=None):
    """
    Given a traversal as a list of nodes, split the traversal
    if a sequential index pair is not in the given edges.

    Parameters
    --------------
    edges : (n, 2) int
       Graph edge indexes
    traversal : (m,) int
       Traversal through edges
    edge_hash : (n,)
       Edges sorted on axis=1 and
       passed to grouping.hashable_rows

    Returns
    ---------------
    split : sequence of (p,) int
    """
    traversal = np.asanyarray(traversal,
                              dtype=np.int64)

    # hash edge rows for contains checks
    if edges_hash is None:
        edges_hash = grouping.hashable_rows(
            np.sort(edges, axis=1))

    # turn the (n,) traversal into (n-1, 2) edges
    trav_edge = np.column_stack((traversal[:-1],
                                 traversal[1:]))
    # hash each edge so we can compare to edge set
    trav_hash = grouping.hashable_rows(
        np.sort(trav_edge, axis=1))
    # check if each edge is contained in edge set
    contained = np.in1d(trav_hash, edges_hash)

    # exit early if every edge of traversal exists
    if contained.all():
        # just reshape one traversal
        split = [traversal]
    else:
        # find contiguous groups of contained edges
        blocks = grouping.blocks(contained,
                                 min_len=1,
                                 only_nonzero=True)

        # turn edges back in to sequence of traversals
        split = [np.append(trav_edge[b][:, 0],
                           trav_edge[b[-1]][1])
                 for b in blocks]

    # close traversals if necessary
    for i, t in enumerate(split):
        # make sure elements of sequence are numpy arrays
        split[i] = np.asanyarray(split[i], dtype=np.int64)
        # don't close if its a single edge
        if len(t) <= 2:
            continue
        # make sure it's not already closed
        edge = np.sort([t[0], t[-1]])
        if edge.ptp() == 0:
            continue
        close = grouping.hashable_rows(edge.reshape((1, 2)))[0]
        # if we need the edge add it
        if close in edges_hash:
            split[i] = np.append(t, t[0]).astype(np.int64)

    return split


def fill_traversals(traversals, edges, edges_hash=None):
    """
    Convert a traversal of a list of edges into a sequence of
    traversals where every pair of consecutive node indexes
    is an edge in a passed edge list

    Parameters
    -------------
    traversals : sequence of (m,) int
       Node indexes of traversals of a graph
    edges : (n, 2) int
       Pairs of connected node indexes
    edges_hash : None, or (n,) int
       Edges sorted along axis 1 then hashed
       using grouping.hashable_rows

    Returns
    --------------
    splits : sequence of (p,) int
       Node indexes of connected traversals
    """
    # make sure edges are correct type
    edges = np.asanyarray(edges, dtype=np.int64)
    # make sure edges are sorted
    edges.sort(axis=1)

    # if there are no traversals just return edges
    if len(traversals) == 0:
        return edges.copy()

    # hash edges for contains checks
    if edges_hash is None:
        edges_hash = grouping.hashable_rows(edges)

    splits = []
    for nodes in traversals:
        # split traversals to remove edges
        # that don't actually exist
        splits.extend(split_traversal(
            traversal=nodes,
            edges=edges,
            edges_hash=edges_hash))
    # turn the split traversals back into (n, 2) edges
    included = util.vstack_empty([np.column_stack((i[:-1], i[1:]))
                                  for i in splits])
    if len(included) > 0:
        # sort included edges in place
        included.sort(axis=1)
        # make sure any edges not included in split traversals
        # are just added as a length 2 traversal
        splits.extend(grouping.boolean_rows(
            edges,
            included,
            operation=np.setdiff1d))
    else:
        # no edges were included, so our filled traversal
        # is just the original edges copied over
        splits = edges.copy()

    return splits


def traversals(edges, mode='bfs'):
    """
    Given an edge list generate a sequence of ordered depth
    first search traversals using scipy.csgraph routines.

    Parameters
    ------------
    edges : (n, 2) int
      Undirected edges of a graph
    mode :  str
      Traversal type, 'bfs' or 'dfs'

    Returns
    -----------
    traversals : (m,) sequence of (p,) int
      Ordered DFS or BFS traversals of the graph.
    """
    edges = np.array(edges, dtype=np.int64)
    if len(edges) == 0:
        return []
    elif not util.is_shape(edges, (-1, 2)):
        raise ValueError('edges are not (n, 2)!')

    # pick the traversal method
    mode = str(mode).lower().strip()
    if mode == 'bfs':
        func = csgraph.breadth_first_order
    elif mode == 'dfs':
        func = csgraph.depth_first_order
    else:
        raise ValueError('traversal mode must be either dfs or bfs')

    # make sure edges are sorted so we can query
    # an ordered pair later
    edges.sort(axis=1)
    # set of nodes to make sure we get every node
    nodes = set(edges.reshape(-1))
    # coo_matrix for csgraph routines
    graph = edges_to_coo(edges)

    # we're going to make a sequence of traversals
    traversals = []

    while len(nodes) > 0:
        # starting at any node
        start = nodes.pop()
        # get an (n,) ordered traversal
        ordered = func(graph,
                       i_start=start,
                       return_predecessors=False,
                       directed=False).astype(np.int64)

        traversals.append(ordered)
        # remove the nodes we've consumed
        nodes.difference_update(ordered)

    return traversals


def edges_to_coo(edges, count=None, data=None):
    """
    Given an edge list, return a boolean scipy.sparse.coo_matrix
    representing the edges in matrix form.

    Parameters
    ------------
    edges : (n, 2) int
      Edges of a graph
    count : int
      The total number of nodes in the graph
      if None: count = edges.max() + 1
    data : (n,) any
      Assign data to each edge, if None will
      be bool True for each specified edge

    Returns
    ------------
    matrix: (count, count) scipy.sparse.coo_matrix
      Sparse COO
    """
    edges = np.asanyarray(edges, dtype=np.int64)
    if not (len(edges) == 0 or
            util.is_shape(edges, (-1, 2))):
        raise ValueError('edges must be (n, 2)!')

    # if count isn't specified just set it to largest
    # value referenced in edges
    if count is None:
        count = edges.max() + 1
    count = int(count)

    # if no data is specified set every specified edge
    # to True
    if data is None:
        data = np.ones(len(edges), dtype=bool)

    matrix = coo_matrix((data, edges.T),
                        dtype=data.dtype,
                        shape=(count, count))
    return matrix


def neighbors(edges, max_index=None, directed=False):
    """
    Find the neighbors for each node in an edgelist graph.

    TODO : re-write this with sparse matrix operations

    Parameters
    ------------
    edges : (n, 2) int
      Connected nodes
    directed : bool
      If True, only connect edges in one direction

    Returns
    ---------
    neighbors : sequence
      Vertex index corresponds to set of other vertex indices
    """
    neighbors = collections.defaultdict(set)
    if directed:
        [neighbors[edge[0]].add(edge[1])
         for edge in edges]
    else:
        [(neighbors[edge[0]].add(edge[1]),
          neighbors[edge[1]].add(edge[0]))
         for edge in edges]

    if max_index is None:
        max_index = edges.max() + 1
    array = [list(neighbors[i]) for i in range(max_index)]

    return array


def smoothed(mesh, angle=None, facet_minarea=10):
    """
    Return a non- watertight version of the mesh which
    will render nicely with smooth shading by
    disconnecting faces at sharp angles to each other.

    Parameters
    -----------
    mesh : trimesh.Trimesh
      Source geometry
    angle : float or None
      Angle in radians face pairs with angles
      smaller than this will appear smoothed
    facet_minarea : float or None
      Minimum area fraction to consider
      IE for `facets_minarea=25` only facets larger
      than `mesh.area / 25` will be considered.

    Returns
    ---------
    smooth : trimesh.Trimesh
      Geometry with disconnected face patches
    """
    if angle is None:
        angle = np.radians(20)

    # if the mesh has no adjacent faces return a copy
    if len(mesh.face_adjacency) == 0:
        return mesh.copy()

    # face pairs below angle threshold
    angle_ok = mesh.face_adjacency_angles < angle
    # subset of face adjacency
    adjacency = mesh.face_adjacency[angle_ok]

    # coplanar groups of faces
    facets = []
    nodes = None
    # collect coplanar regions for smoothing
    if facet_minarea is not None:
        areas = mesh.area_faces
        min_area = mesh.area / facet_minarea
        try:
            # we can survive not knowing facets
            # exclude facets with few faces
            facets = [f for f in mesh.facets
                      if areas[f].sum() > min_area]
            if len(facets) > 0:
                # mask for removing adjacency pairs where
                # one of the faces is contained in a facet
                mask = np.ones(len(mesh.faces),
                               dtype=bool)
                mask[np.hstack(facets)] = False
                # apply the mask to adjacency
                adjacency = adjacency[mask[adjacency].all(axis=1)]
                # nodes are no longer every faces
                nodes = np.unique(adjacency)
        except BaseException:
            log.warning('failed to calculate facets',
                        exc_info=True)
    # run connected components on facet adjacency
    components = connected_components(
        adjacency,
        min_len=2,
        nodes=nodes)

    # add back coplanar groups if any exist
    if len(facets) > 0:
        components.extend(facets)

    if len(components) == 0:
        # if no components for some reason
        # just return a copy of the original mesh
        return mesh.copy()

    # add back any faces that were missed
    unique = np.unique(np.hstack(components))
    if len(unique) != len(mesh.faces):
        # things like single loose faces
        # or groups below facet_minlen
        broke = np.setdiff1d(
            np.arange(len(mesh.faces)), unique)
        components.extend(broke.reshape((-1, 1)))

    # get a submesh as a single appended Trimesh
    smooth = mesh.submesh(components,
                          only_watertight=False,
                          append=True)
    # store face indices from original mesh
    smooth.metadata['original_components'] = components
    # smoothed should have exactly the same number of faces
    if len(smooth.faces) != len(mesh.faces):
        log.warning('face count in smooth wrong!')
    return smooth


def is_watertight(edges, edges_sorted=None):
    """
    Parameters
    -----------
    edges : (n, 2) int
      List of vertex indices
    edges_sorted : (n, 2) int
      Pass vertex indices sorted on axis 1 as a speedup

    Returns
    ---------
    watertight : boolean
      Whether every edge is shared by an even
      number of faces
    winding : boolean
      Whether every shared edge is reversed
    """
    # passing edges_sorted is a speedup only
    if edges_sorted is None:
        edges_sorted = np.sort(edges, axis=1)

    # group sorted edges
    groups = grouping.group_rows(
        edges_sorted, require_count=2)
    watertight = bool((len(groups) * 2) == len(edges))

    # are opposing edges reversed
    opposing = edges[groups].reshape((-1, 4))[:, 1:3].T
    # wrap the weird numpy bool
    winding = bool(np.equal(*opposing).all())

    return watertight, winding


def graph_to_svg(graph):
    """
    Turn a networkx graph into an SVG string
    using graphviz `dot`.

    Parameters
    ----------
    graph: networkx graph

    Returns
    ---------
    svg: string, pictoral layout in SVG format
    """

    import tempfile
    import subprocess
    with tempfile.NamedTemporaryFile() as dot_file:
        nx.drawing.nx_agraph.write_dot(graph, dot_file.name)
        svg = subprocess.check_output(['dot', dot_file.name, '-Tsvg'])
    return svg


def multigraph_paths(G, source, cutoff=None):
    """
    For a networkx MultiDiGraph, find all paths from a source node
    to leaf nodes. This function returns edge instance numbers
    in addition to nodes, unlike networkx.all_simple_paths.

    Parameters
    ---------------
    G : networkx.MultiDiGraph
      Graph to evaluate
    source : hashable
      Node to start traversal at
    cutoff : int
      Number of nodes to visit
      If None will visit all nodes

    Returns
    ----------
    traversals : (n,) list of [(node, edge instance index), ] paths
      Traversals of the multigraph
    """
    if cutoff is None:
        cutoff = (len(G.edges()) * len(G.nodes())) + 1

    # the path starts at the node specified
    current = [(source, 0)]
    # traversals we need to go back and do
    queue = []
    # completed paths
    traversals = []

    for _ in range(cutoff):
        # paths are stored as (node, instance) so
        # get the node of the last place visited
        current_node = current[-1][0]
        # get all the children of the current node
        child = G[current_node]

        if len(child) == 0:
            # we have no children, so we are at the end of this path
            # save the path as a completed traversal
            traversals.append(current)
            # if there is nothing on the queue, we are done
            if len(queue) == 0:
                break
            # otherwise continue traversing with the next path
            # on the queue
            current = queue.pop()
        else:
            # oh no, we have multiple edges from current -> child
            start = True
            # iterate through child nodes and edge instances
            for node in child.keys():
                for instance in child[node].keys():
                    if start:
                        # if this is the first edge, keep it on the
                        # current traversal and save the others for later
                        current.append((node, instance))
                        start = False
                    else:
                        # this child has multiple instances
                        # so we will need to traverse them multiple times
                        # we appended a node to current, so only take the
                        # first n-1 visits
                        queue.append(current[:-1] + [(node, instance)])
    return traversals


def multigraph_collect(G, traversal, attrib=None):
    """
    Given a MultiDiGraph traversal, collect attributes along it.

    Parameters
    -------------
    G:          networkx.MultiDiGraph
    traversal:  (n) list of (node, instance) tuples
    attrib:     dict key, name to collect. If None, will return all

    Returns
    -------------
    collected: (len(traversal) - 1) list of attributes
    """

    collected = []
    for u, v in util.pairwise(traversal):
        attribs = G[u[0]][v[0]][v[1]]
        if attrib is None:
            collected.append(attribs)
        else:
            collected.append(attribs[attrib])
    return collected


# trimesh/grouping.py

"""
grouping.py
-------------

Functions for grouping values and rows.
"""

import numpy as np

from . import util

from .constants import log, tol

try:
    from scipy.spatial import cKDTree
except BaseException as E:
    # wrapping just ImportError fails in some cases
    # will raise the error when someone tries to use KDtree
    from . import exceptions
    cKDTree = exceptions.ExceptionWrapper(E)


def merge_vertices(mesh,
                   merge_tex=None,
                   merge_norm=None,
                   digits_vertex=None,
                   digits_norm=None,
                   digits_uv=None):
    """
    Removes duplicate vertices, grouped by position and
    optionally texture coordinate and normal.

    Parameters
    -------------
    mesh : Trimesh object
      Mesh to merge vertices on
    merge_tex : bool
      If True textured meshes with UV coordinates will
      have vertices merged regardless of UV coordinates
    merge_norm : bool
      If True, meshes with vertex normals will have
      vertices merged ignoring different normals
    digits_vertex : None or int
      Number of digits to consider for vertex position
    digits_norm : int
      Number of digits to consider for unit normals
    digits_uv : int
      Number of digits to consider for UV coordinates
    """
    # no vertices so exit early
    if len(mesh.vertices) == 0:
        return
    if merge_tex is None:
        merge_tex = False
    if merge_norm is None:
        merge_norm = False
    if digits_norm is None:
        digits_norm = 2
    if digits_uv is None:
        digits_uv = 4
    if digits_vertex is None:
        # use tol.merge if digit precision not passed
        digits_vertex = util.decimal_to_digits(
            tol.merge)

    # if we have a ton of unreferenced vertices it will
    # make the unique_rows call super slow so cull first
    if hasattr(mesh, 'faces') and len(mesh.faces) > 0:
        referenced = np.zeros(len(mesh.vertices), dtype=bool)
        referenced[mesh.faces] = True
    else:
        # this is used for geometry without faces
        referenced = np.ones(len(mesh.vertices), dtype=bool)

    # collect vertex attributes into sequence we can stack
    stacked = [mesh.vertices * (10 ** digits_vertex)]

    # UV texture visuals require us to update the
    # vertices and normals differently
    if (not merge_tex and
        mesh.visual.defined and
        mesh.visual.kind == 'texture' and
        mesh.visual.uv is not None and
            len(mesh.visual.uv) == len(mesh.vertices)):
        # get an array with vertices and UV coordinates
        # converted to integers at requested precision
        stacked.append(mesh.visual.uv * (10 ** digits_uv))

    # check to see if we have vertex normals
    normals = mesh._cache['vertex_normals']
    if not merge_norm and np.shape(normals) == mesh.vertices.shape:
        stacked.append(normals * (10 ** digits_norm))

    # stack collected vertex properties and round to integer
    stacked = np.column_stack(stacked).round().astype(np.int64)

    # check unique rows of referenced vertices
    u, i = unique_rows(stacked[referenced], keep_order=True)

    # construct an inverse using the subset
    inverse = np.zeros(len(mesh.vertices), dtype=np.int64)
    inverse[referenced] = i
    # get the vertex mask
    mask = np.nonzero(referenced)[0][u]
    # run the update including normals and UV coordinates
    mesh.update_vertices(mask=mask, inverse=inverse)


def group(values, min_len=0, max_len=np.inf):
    """
    Return the indices of values that are identical

    Parameters
    ----------
    values : (n,) int
      Values to group
    min_len : int
      The shortest group allowed
      All groups will have len >= min_length
    max_len : int
      The longest group allowed
      All groups will have len <= max_length

    Returns
    ----------
    groups : sequence
      Contains indices to form groups
      IE [0,1,0,1] returns [[0,2], [1,3]]
    """
    original = np.asanyarray(values)

    # save the sorted order and then apply it
    order = original.argsort()
    values = original[order]

    # find the indexes which are duplicates
    if values.dtype.kind == 'f':
        # for floats in a sorted array, neighbors are not duplicates
        # if the difference between them is greater than approximate zero
        nondupe = np.greater(np.abs(np.diff(values)), tol.zero)
    else:
        # for ints and strings we can check exact non- equality
        # for all other types this will only work if they defined
        # an __eq__
        nondupe = values[1:] != values[:-1]

    dupe_idx = np.append(0, np.nonzero(nondupe)[0] + 1)
    dupe_len = np.diff(np.concatenate((dupe_idx, [len(values)])))
    dupe_ok = np.logical_and(np.greater_equal(dupe_len, min_len),
                             np.less_equal(dupe_len, max_len))
    groups = [order[i:(i + j)]
              for i, j in zip(dupe_idx[dupe_ok],
                              dupe_len[dupe_ok])]
    return groups


def hashable_rows(data, digits=None):
    """
    We turn our array into integers based on the precision
    given by digits and then put them in a hashable format.

    Parameters
    ---------
    data : (n, m) array
      Input data
    digits : int or None
      How many digits to add to hash if data is floating point
      If None, tol.merge will be used

    Returns
    ---------
    hashable : (n,) array
      Custom data type which can be sorted
      or used as hash keys
    """
    # if there is no data return immediately
    if len(data) == 0:
        return np.array([])

    # get array as integer to precision we care about
    as_int = float_to_int(data, digits=digits)

    # if it is flat integers already, return
    if len(as_int.shape) == 1:
        return as_int

    # if array is 2D and smallish, we can try bitbanging
    # this is significantly faster than the custom dtype
    if len(as_int.shape) == 2 and as_int.shape[1] <= 4:
        # time for some righteous bitbanging
        # can we pack the whole row into a single 64 bit integer
        precision = int(np.floor(64 / as_int.shape[1]))
        # if the max value is less than precision we can do this
        if np.abs(as_int).max() < 2**(precision - 1):
            # the resulting package
            hashable = np.zeros(len(as_int), dtype=np.int64)
            # loop through each column and bitwise xor to combine
            # make sure as_int is int64 otherwise bit offset won't work
            for offset, column in enumerate(as_int.astype(np.int64).T):
                # will modify hashable in place
                np.bitwise_xor(hashable,
                               column << (offset * precision),
                               out=hashable)
            return hashable

    # reshape array into magical data type that is weird but hashable
    dtype = np.dtype((np.void, as_int.dtype.itemsize * as_int.shape[1]))
    # make sure result is contiguous and flat
    hashable = np.ascontiguousarray(as_int).view(dtype).reshape(-1)
    return hashable


def float_to_int(data, digits=None, dtype=np.int32):
    """
    Given a numpy array of float/bool/int, return as integers.

    Parameters
    -------------
    data :  (n, d) float, int, or bool
      Input data
    digits : float or int
      Precision for float conversion
    dtype : numpy.dtype
      What datatype should result be returned as

    Returns
    -------------
    as_int : (n, d) int
      Data as integers
    """
    # convert to any numpy array
    data = np.asanyarray(data)

    # if data is already an integer or boolean we're done
    # if the data is empty we are also done
    if data.dtype.kind in 'ib' or data.size == 0:
        return data.astype(dtype)
    elif data.dtype.kind != 'f':
        data = data.astype(np.float64)

    # populate digits from kwargs
    if digits is None:
        digits = util.decimal_to_digits(tol.merge)
    elif isinstance(digits, float) or isinstance(digits, np.float64):
        digits = util.decimal_to_digits(digits)
    elif not (isinstance(digits, int) or isinstance(digits, np.integer)):
        log.warning('Digits were passed as %s!', digits.__class__.__name__)
        raise ValueError('Digits must be None, int, or float!')

    # data is float so convert to large integers
    data_max = np.abs(data).max() * 10**digits
    # ignore passed dtype if we have something large
    dtype = [np.int32, np.int64][int(data_max > 2**31)]
    # multiply by requested power of ten
    # then subtract small epsilon to avoid "go either way" rounding
    # then do the rounding and convert to integer
    as_int = np.round((data * 10 ** digits) - 1e-6).astype(dtype)

    return as_int


def unique_ordered(data, return_index=False, return_inverse=False):
    """
    Returns the same as np.unique, but ordered as per the
    first occurrence of the unique value in data.

    Examples
    ---------
    In [1]: a = [0, 3, 3, 4, 1, 3, 0, 3, 2, 1]

    In [2]: np.unique(a)
    Out[2]: array([0, 1, 2, 3, 4])

    In [3]: trimesh.grouping.unique_ordered(a)
    Out[3]: array([0, 3, 4, 1, 2])
    """
    # uniques are the values, sorted
    # index is the value in the original `data`
    # i.e. `data[index] == unique`
    # inverse is how to re-construct `data` from `unique`
    # i.e. `unique[inverse] == data`
    unique, index, inverse = np.unique(
        data, return_index=True, return_inverse=True)

    # we want to maintain the original index order
    order = index.argsort()

    if not return_index and not return_inverse:
        return unique[order]

    # collect return values
    # start with the unique values in original order
    result = [unique[order]]
    # the new index values
    if return_index:
        # re-order the index in the original array
        result.append(index[order])
    if return_inverse:
        # create the new inverse from the order of the order
        result.append(order.argsort()[inverse])

    return result


def unique_bincount(values,
                    minlength=0,
                    return_inverse=False,
                    return_counts=False):
    """
    For arrays of integers find unique values using bin counting.
    Roughly 10x faster for correct input than np.unique

    Parameters
    --------------
    values : (n,) int
      Values to find unique members of
    minlength : int
      Maximum value that will occur in values (values.max())
    return_inverse : bool
      If True, return an inverse such that unique[inverse] == values
    return_counts : bool
      If True, also return the number of times each
      unique item appears in values

    Returns
    ------------
    unique : (m,) int
      Unique values in original array
    inverse : (n,) int, optional
      An array such that unique[inverse] == values
      Only returned if return_inverse is True
    counts : (m,) int, optional
      An array holding the counts of each unique item in values
      Only returned if return_counts is True
    """
    values = np.asanyarray(values)
    if len(values.shape) != 1 or values.dtype.kind != 'i':
        raise ValueError('input must be 1D integers!')

    try:
        # count the number of occurrences of each value
        counts = np.bincount(values, minlength=minlength)
    except TypeError:
        # casting failed on 32 bit windows
        log.warning('casting failed, falling back!')
        # fall back to numpy unique
        return np.unique(values,
                         return_inverse=return_inverse,
                         return_counts=return_counts)

    # which bins are occupied at all
    # counts are integers so this works
    unique_bin = counts.astype(bool)

    # which values are unique
    # indexes correspond to original values
    unique = np.where(unique_bin)[0]
    ret = (unique,)

    if return_inverse:
        # find the inverse to reconstruct original
        inverse = (np.cumsum(unique_bin) - 1)[values]
        ret += (inverse,)

    if return_counts:
        unique_counts = counts[unique]
        ret += (unique_counts,)

    if len(ret) == 1:
        return ret[0]
    return ret


def merge_runs(data, digits=None):
    """
    Merge duplicate sequential values. This differs from unique_ordered
    in that values can occur in multiple places in the sequence, but
    only consecutive repeats are removed

    Parameters
    -----------
    data: (n,) float or int

    Returns
    --------
    merged: (m,) float or int

    Examples
    ---------
    In [1]: a
    Out[1]:
    array([-1, -1, -1,  0,  0,  1,  1,  2,  0,
            3,  3,  4,  4,  5,  5,  6,  6,  7,
            7,  8,  8,  9,  9,  9])

    In [2]: trimesh.grouping.merge_runs(a)
    Out[2]: array([-1,  0,  1,  2,  0,  3,  4,  5,  6,  7,  8,  9])
    """
    data = np.asanyarray(data)
    mask = np.empty(len(data), dtype=bool)
    mask[0] = True
    mask[1:] = np.abs(data[1:] - data[:-1]) > tol.merge

    return data[mask]


def unique_float(data,
                 return_index=False,
                 return_inverse=False,
                 digits=None):
    """
    Identical to the numpy.unique command, except evaluates floating point
    numbers, using a specified number of digits.

    If digits isn't specified, the library default TOL_MERGE will be used.
    """
    data = np.asanyarray(data)
    as_int = float_to_int(data, digits)
    _junk, unique, inverse = np.unique(as_int,
                                       return_index=True,
                                       return_inverse=True)

    if (not return_index) and (not return_inverse):
        return data[unique]

    result = [data[unique]]

    if return_index:
        result.append(unique)
    if return_inverse:
        result.append(inverse)
    return tuple(result)


def unique_rows(data, digits=None, keep_order=False):
    """
    Returns indices of unique rows. It will return the
    first occurrence of a row that is duplicated:
    [[1,2], [3,4], [1,2]] will return [0,1]

    Parameters
    ---------
    data : (n, m) array
      Floating point data
    digits : int or None
      How many digits to consider

    Returns
    --------
    unique :  (j,) int
      Index in data which is a unique row
    inverse : (n,) int
      Array to reconstruct original
      Example: data[unique][inverse] == data
    """
    # get rows hashable so we can run unique function on it
    rows = hashable_rows(data, digits=digits)

    # we are throwing away the first value which is the
    # garbage row-hash and only returning index and inverse
    if keep_order:
        # keeps order of original occurrence
        return unique_ordered(
            rows, return_index=True, return_inverse=True)[1:]
    # returns values sorted by row-hash but since our row-hash
    # were pretty much garbage the sort order isn't meaningful
    return np.unique(rows, return_index=True, return_inverse=True)[1:]


def unique_value_in_row(data, unique=None):
    """
    For a 2D array of integers find the position of a
    value in each row which only occurs once.

    If there are more than one value per row which
    occur once, the last one is returned.

    Parameters
    ----------
    data :   (n, d) int
      Data to check values
    unique : (m,) int
      List of unique values contained in data.
      Generated from np.unique if not passed

    Returns
    ---------
    result : (n, d) bool
      With one or zero True values per row.


    Examples
    -------------------------------------
    In [0]: r = np.array([[-1,  1,  1],
                          [-1,  1, -1],
                          [-1,  1,  1],
                          [-1,  1, -1],
                          [-1,  1, -1]], dtype=np.int8)

    In [1]: unique_value_in_row(r)
    Out[1]:
           array([[ True, False, False],
                  [False,  True, False],
                  [ True, False, False],
                  [False,  True, False],
                  [False,  True, False]], dtype=bool)

    In [2]: unique_value_in_row(r).sum(axis=1)
    Out[2]: array([1, 1, 1, 1, 1])

    In [3]: r[unique_value_in_row(r)]
    Out[3]: array([-1,  1, -1,  1,  1], dtype=int8)
    """
    if unique is None:
        unique = np.unique(data)
    data = np.asanyarray(data)
    result = np.zeros_like(data, dtype=bool, subok=False)
    for value in unique:
        test = np.equal(data, value)
        test_ok = test.sum(axis=1) == 1
        result[test_ok] = test[test_ok]
    return result


def group_rows(data, require_count=None, digits=None):
    """
    Returns index groups of duplicate rows, for example:
    [[1,2], [3,4], [1,2]] will return [[0,2], [1]]


    Note that using require_count allows numpy advanced
    indexing to be used in place of looping and
    checking hashes and is ~10x faster.


    Parameters
    ----------
    data : (n, m) array
      Data to group
    require_count : None or int
      Only return groups of a specified length, eg:
      require_count =  2
      [[1,2], [3,4], [1,2]] will return [[0,2]]
    digits : None or int
    If data is floating point how many decimals
    to consider, or calculated from tol.merge

    Returns
    ----------
    groups : sequence (*,) int
      Indices from in indicating identical rows.
    """

    def group_dict():
        """
        Simple hash table based grouping.
        The loop and appends make this rather slow on
        large arrays but it works on irregular groups.
        """
        observed = dict()
        hashable = hashable_rows(data, digits=digits)
        for index, key in enumerate(hashable):
            key_string = key.tobytes()
            if key_string in observed:
                observed[key_string].append(index)
            else:
                observed[key_string] = [index]
        return list(observed.values())

    def group_slice():
        # create a representation of the rows that can be sorted
        hashable = hashable_rows(data, digits=digits)
        # record the order of the rows so we can get the original indices back
        # later
        order = np.argsort(hashable)
        # but for now, we want our hashes sorted
        hashable = hashable[order]
        # this is checking each neighbour for equality, example:
        # example: hashable = [1, 1, 1]; dupe = [0, 0]
        dupe = hashable[1:] != hashable[:-1]
        # we want the first index of a group, so we can slice from that location
        # example: hashable = [0 1 1]; dupe = [1,0]; dupe_idx = [0,1]
        dupe_idx = np.append(0, np.nonzero(dupe)[0] + 1)
        # if you wanted to use this one function to deal with non- regular groups
        # you could use: np.array_split(dupe_idx)
        # this is roughly 3x slower than using the group_dict method above.
        start_ok = np.diff(
            np.concatenate((dupe_idx, [len(hashable)]))) == require_count
        groups = np.tile(dupe_idx[start_ok].reshape((-1, 1)),
                         require_count) + np.arange(require_count)
        groups_idx = order[groups]
        if require_count == 1:
            return groups_idx.reshape(-1)
        return groups_idx

    if require_count is None:
        return group_dict()
    else:
        return group_slice()


def boolean_rows(a, b, operation=np.intersect1d):
    """
    Find the rows in two arrays which occur in both rows.

    Parameters
    ---------
    a: (n, d) int
        Array with row vectors
    b: (m, d) int
        Array with row vectors
    operation : function
        Numpy boolean set operation function:
          -np.intersect1d
          -np.setdiff1d

    Returns
    --------
    shared: (p, d) array containing rows in both a and b
    """
    a = np.asanyarray(a, dtype=np.int64)
    b = np.asanyarray(b, dtype=np.int64)

    av = a.view([('', a.dtype)] * a.shape[1]).ravel()
    bv = b.view([('', b.dtype)] * b.shape[1]).ravel()
    shared = operation(av, bv).view(a.dtype).reshape(-1, a.shape[1])

    return shared


def group_vectors(vectors,
                  angle=1e-4,
                  include_negative=False):
    """
    Group vectors based on an angle tolerance, with the option to
    include negative vectors.

    Parameters
    -----------
    vectors : (n,3) float
        Direction vector
    angle : float
        Group vectors closer than this angle in radians
    include_negative : bool
        If True consider the same:
        [0,0,1] and [0,0,-1]

    Returns
    ------------
    new_vectors : (m,3) float
        Direction vector
    groups : (m,) sequence of int
        Indices of source vectors
    """

    vectors = np.asanyarray(vectors, dtype=np.float64)
    angle = float(angle)

    if include_negative:
        vectors = util.vector_hemisphere(vectors)

    spherical = util.vector_to_spherical(vectors)
    angles, groups = group_distance(spherical, angle)
    new_vectors = util.spherical_to_vector(angles)
    return new_vectors, groups


def group_distance(values, distance):
    """
    Find groups of points which have neighbours closer than radius,
    where no two points in a group are farther than distance apart.

    Parameters
    ---------
    points :   (n, d) float
        Points of dimension d
    distance : float
        Max distance between points in a cluster

    Returns
    ----------
    unique : (m, d) float
        Median value of each group
    groups : (m) sequence of int
        Indexes of points that make up a group

    """
    values = np.asanyarray(values,
                           dtype=np.float64)

    consumed = np.zeros(len(values),
                        dtype=bool)
    tree = cKDTree(values)

    # (n, d) set of values that are unique
    unique = []
    # (n) sequence of indices in values
    groups = []

    for index, value in enumerate(values):
        if consumed[index]:
            continue
        group = np.array(tree.query_ball_point(value, distance),
                         dtype=np.int64)
        consumed[group] = True
        unique.append(np.median(values[group], axis=0))
        groups.append(group)
    return np.array(unique), groups


def clusters(points, radius):
    """
    Find clusters of points which have neighbours closer than radius

    Parameters
    ---------
    points : (n, d) float
        Points of dimension d
    radius : float
        Max distance between points in a cluster

    Returns
    ----------
    groups : (m,) sequence of int
        Indices of points in a cluster

    """
    from . import graph
    tree = cKDTree(points)

    # some versions return pairs as a set of tuples
    pairs = tree.query_pairs(r=radius, output_type='ndarray')
    # group connected components
    groups = graph.connected_components(pairs)

    return groups


def blocks(data,
           min_len=2,
           max_len=np.inf,
           wrap=False,
           digits=None,
           only_nonzero=False):
    """
    Find the indices in an array of contiguous blocks
    of equal values.

    Parameters
    ------------
    data : (n,) array
      Data to find blocks on
    min_len : int
      The minimum length group to be returned
    max_len : int
      The maximum length group to be retuurned
    wrap : bool
      Combine blocks on both ends of 1D array
    digits : None or int
      If dealing with floats how many digits to consider
    only_nonzero : bool
      Only return blocks of non- zero values

    Returns
    ---------
    blocks : (m) sequence of (*,) int
      Indices referencing data
    """
    data = float_to_int(data, digits=digits)

    # keep an integer range around so we can slice
    arange = np.arange(len(data))
    arange.flags['WRITEABLE'] = False

    nonzero = arange[1:][data[1:] != data[:-1]]
    infl = np.zeros(len(nonzero) + 2, dtype=int)
    infl[-1] = len(data)
    infl[1:-1] = nonzero

    # the length of each chunk
    infl_len = infl[1:] - infl[:-1]

    # check the length of each group
    infl_ok = np.logical_and(infl_len >= min_len,
                             infl_len <= max_len)

    if only_nonzero:
        # check to make sure the values of each contiguous block
        # are True by checking the first value of each block
        infl_ok = np.logical_and(
            infl_ok, data[infl[:-1]])

    # inflate start/end indexes into full ranges of values
    blocks = [arange[infl[i]:infl[i + 1]]
              for i, ok in enumerate(infl_ok) if ok]

    if wrap:
        # wrap only matters if first and last points are the same
        if data[0] != data[-1]:
            return blocks
        # if we are only grouping nonzero things and
        # the first and last point are zero we can exit
        if only_nonzero and not bool(data[0]):
            return blocks

        # so now first point equals last point, so the cases are:
        # - first and last point are in a block: combine two blocks
        # - first OR last point are in block: add other point to block
        # - neither are in a block: check if combined is eligible block

        # first point is in a block
        first = len(blocks) > 0 and blocks[0][0] == 0
        # last point is in a block
        last = len(blocks) > 0 and blocks[-1][-1] == (len(data) - 1)

        # CASE: first and last point are BOTH in block: combine blocks
        if first and last:
            blocks[0] = np.append(blocks[-1], blocks[0])
            blocks.pop()
        else:
            # combined length
            combined = infl_len[0] + infl_len[-1]
            # exit if lengths aren't OK
            if combined < min_len or combined > max_len:
                return blocks
            # new block combines both ends
            new_block = np.append(np.arange(infl[-2], infl[-1]),
                                  np.arange(infl[0], infl[1]))
            # we are in a first OR last situation now
            if first:
                # first was already in a block so replace it with combined
                blocks[0] = new_block
            elif last:
                # last was already in a block so replace with superset
                blocks[-1] = new_block
            else:
                # both are false
                # combined length generated new block
                blocks.append(new_block)

    return blocks


def group_min(groups, data):
    """
    Given a list of groups find the minimum element of data
    within each group

    Parameters
    -----------
    groups : (n,) sequence of (q,) int
        Indexes of each group corresponding to each element in data
    data : (m,)
        The data that groups indexes reference

    Returns
    -----------
    minimums : (n,)
        Minimum value of data per group

    """
    # sort with major key groups, minor key data
    order = np.lexsort((data, groups))
    groups = groups[order]  # this is only needed if groups is unsorted
    data = data[order]
    # construct an index which marks borders between groups
    index = np.empty(len(groups), 'bool')
    index[0] = True
    index[1:] = groups[1:] != groups[:-1]
    return data[index]


# trimesh/inertia.py

"""
inertia.py
-------------

Functions for dealing with inertia tensors.

Results validated against known geometries and checked for
internal consistency.
"""

import numpy as np

from trimesh import util


def cylinder_inertia(mass, radius, height, transform=None):
    """
    Return the inertia tensor of a cylinder.

    Parameters
    ------------
    mass : float
      Mass of cylinder
    radius : float
      Radius of cylinder
    height : float
      Height of cylinder
    transform : (4, 4) float
      Transformation of cylinder

    Returns
    ------------
    inertia : (3, 3) float
      Inertia tensor
    """
    h2, r2 = height ** 2, radius ** 2
    diagonal = np.array([((mass * h2) / 12) + ((mass * r2) / 4),
                         ((mass * h2) / 12) + ((mass * r2) / 4),
                         (mass * r2) / 2])
    inertia = diagonal * np.eye(3)

    if transform is not None:
        inertia = transform_inertia(transform, inertia)

    return inertia


def sphere_inertia(mass, radius):
    """
    Return the inertia tensor of a sphere.

    Parameters
    ------------
    mass : float
      Mass of sphere
    radius : float
      Radius of sphere

    Returns
    ------------
    inertia : (3, 3) float
      Inertia tensor
    """
    inertia = (2.0 / 5.0) * (radius ** 2) * mass * np.eye(3)
    return inertia


def principal_axis(inertia):
    """
    Find the principal components and principal axis
    of inertia from the inertia tensor.

    Parameters
    ------------
    inertia : (3, 3) float
      Inertia tensor

    Returns
    ------------
    components : (3,) float
      Principal components of inertia
    vectors : (3, 3) float
      Row vectors pointing along the
      principal axes of inertia
    """
    inertia = np.asanyarray(inertia, dtype=np.float64)
    if inertia.shape != (3, 3):
        raise ValueError('inertia tensor must be (3, 3)!')

    # you could any of the following to calculate this:
    # np.linalg.svd, np.linalg.eig, np.linalg.eigh
    # moment of inertia is square symmetric matrix
    # eigh has the best numeric precision in tests
    components, vectors = np.linalg.eigh(inertia)

    # eigh returns them as column vectors, change them to row vectors
    vectors = vectors.T

    return components, vectors


def transform_inertia(transform,
                      inertia_tensor,
                      parallel_axis=False,
                      mass=None):
    """
    Transform an inertia tensor to a new frame.

    Note that in trimesh `mesh.moment_inertia` is *axis aligned*
    and at `mesh.center_mass`.

    So to transform to a new frame and get the moment of inertia at
    the center of mass the translation should be ignored and only
    rotation applied.

    If parallel axis is enabled it will compute the inertia
    about a new location.

    More details in the MIT OpenCourseWare PDF:
   ` MIT16_07F09_Lec26.pdf`


    Parameters
    ------------
    transform : (3, 3) or (4, 4) float
      Transformation matrix
    inertia_tensor : (3, 3) float
      Inertia tensor.
    parallel_axis : bool
      Apply the parallel axis theorum or not.
      If the passed inertia tensor is at the center of mass
      and you want the new post-transform tensor also at the
      center of mass you DON'T want this enabled as you *only*
      want to apply the rotation. Use this to get moment of
      inertia at an arbitrary frame that isn't the center of mass.

    Returns
    ------------
    transformed : (3, 3) float
      Inertia tensor in new frame.
    """
    # check inputs and extract rotation
    transform = np.asanyarray(transform, dtype=np.float64)
    if transform.shape == (4, 4):
        rotation = transform[:3, :3]
    elif transform.shape == (3, 3):
        rotation = transform
    else:
        raise ValueError('transform must be (3, 3) or (4, 4)!')

    inertia_tensor = np.asanyarray(inertia_tensor, dtype=np.float64)
    if inertia_tensor.shape != (3, 3):
        raise ValueError('inertia_tensor must be (3, 3)!')

    if parallel_axis:
        if transform.shape == (3, 3):
            # shorthand for "translation"
            a = np.zeros(3, dtype=np.float64)
        else:
            # get the translation
            a = transform[:3, 3]
        # First the changed origin of the new transform is taken into
        # account. To calculate the inertia tensor
        # the parallel axis theorem is used
        M = np.array([[a[1]**2 + a[2]**2, -a[0] * a[1], -a[0] * a[2]],
                      [-a[0] * a[1], a[0]**2 + a[2]**2, -a[1] * a[2]],
                      [-a[0] * a[2], -a[1] * a[2], a[0]**2 + a[1]**2]])
        aligned_inertia = inertia_tensor + mass * M

        return util.multi_dot([rotation.T,
                               aligned_inertia,
                               rotation])

    return util.multi_dot([rotation,
                           inertia_tensor,
                           rotation.T])


def radial_symmetry(mesh):
    """
    Check whether a mesh has radial symmetry.

    Returns
    -----------
    symmetry : None or str
         None         No rotational symmetry
         'radial'     Symmetric around an axis
         'spherical'  Symmetric around a point
    axis : None or (3,) float
      Rotation axis or point
    section : None or (3, 2) float
      If radial symmetry provide vectors
      to get cross section
    """

    # shortcuts to avoid typing and hitting cache
    scalar = mesh.principal_inertia_components.copy()

    # exit early if inertia components are all zero
    if (scalar < 1e-12).any():
        return None, None, None

    # normalize the PCI so we can compare them
    scalar = scalar / np.linalg.norm(scalar)
    vector = mesh.principal_inertia_vectors
    # the sorted order of the principal components
    order = scalar.argsort()

    # we are checking if a geometry has radial symmetry
    # if 2 of the PCI are equal, it is a revolved 2D profile
    # if 3 of the PCI (all of them) are equal it is a sphere
    diff = np.abs(np.diff(scalar[order]))
    # diffs that are within tol of zero
    diff_zero = diff < 1e-4

    if diff_zero.all():
        # this is the case where all 3 PCI are identical
        # this means that the geometry is symmetric about a point
        # examples of this are a sphere, icosahedron, etc
        axis = vector[0]
        section = vector[1:]

        return 'spherical', axis, section

    elif diff_zero.any():
        # this is the case for 2/3 PCI are identical
        # this means the geometry is symmetric about an axis
        # probably a revolved 2D profile

        # we know that only 1/2 of the diff values are True
        # if the first diff is 0, it means if we take the first element
        # in the ordered PCI we will have one of the non- revolve axis
        # if the second diff is 0, we take the last element of
        # the ordered PCI for the section axis
        # if we wanted the revolve axis we would just switch [0,-1] to
        # [-1,0]

        # since two vectors are the same, we know the middle
        # one is one of those two
        section_index = order[
            np.array([[0, 1], [1, -1]])[diff_zero]].flatten()
        section = vector[section_index]

        # we know the rotation axis is the sole unique value
        # and is either first or last of the sorted values
        axis_index = order[np.array([-1, 0])[diff_zero]][0]
        axis = vector[axis_index]
        return 'radial', axis, section

    return None, None, None


def scene_inertia(scene, transform):
    """
    Calculate the inertia of a scene about a specific frame.

    Parameters
    ------------
    scene : trimesh.Scene
      Scene with geometry.
    transform : None or (4, 4) float
      Homogeneous transform to compute inertia at.
    """
    # shortcuts for tight loop
    graph = scene.graph
    geoms = scene.geometry

    # get the matrix ang geometry name for
    nodes = [graph[n] for n in graph.nodes_geometry]
    # get the moment of inertia with the mesh moved to a location
    moments = np.array([geoms[g].moment_inertia_frame(
        np.dot(np.linalg.inv(mat), transform)) for mat, g in nodes
        if hasattr(geoms[g], 'moment_inertia_frame')],
        dtype=np.float64)

    return moments.sum(axis=0)


# trimesh/integrate.py

"""
integrate.py
--------------

Utilities for integrating functions over meshes surfaces.
"""

import sympy as sp
from sympy.parsing.sympy_parser import parse_expr as sympy_parse

from .constants import log
from . import util

import warnings
warnings.warn(
    '`trimesh.integrate` is deprecated and will ' +
    'be removed in March 2024, you might take a ' +
    'look at `quadpy` which is far more complete',
    category=DeprecationWarning,
    stacklevel=2)


def symbolic_barycentric(function):
    """
    Symbolically integrate a function(x,y,z) across a triangle or mesh.

    Parameters
    ----------
    function: string or sympy expression
              x, y, z will be replaced with a barycentric representation
              and the the function is integrated across the triangle.

    Returns
    ----------
    evaluator: numpy lambda function of result which takes a mesh
    expr:      sympy expression of result

    Examples
    -----------

    In [1]: function = '1'

    In [2]: integrator, expr = integrate_barycentric(function)

    In [3]: integrator
    Out[3]: <__main__.evaluator instance at 0x7f66cd2a6200>

    In [4]: expr
    Out[4]: 1/2

    In [5]: result  = integrator(mesh)

    In [6]: mesh.area
    Out[6]: 34.641016151377542

    In [7]: result.sum()
    Out[7]: 34.641016151377542
    """

    class evaluator:

        def __init__(self, expr, expr_args):
            self.lambdified = sp.lambdify(args=expr_args,
                                          expr=expr,
                                          modules=['numpy', 'sympy'])

        def __call__(self, mesh, *args):
            """
            Quickly evaluate the surface integral across a mesh

            Parameters
            ----------
            mesh: Trimesh object

            Returns
            ----------
            integrated: (len(faces),) float, integral evaluated for each face
            """
            integrated = self.lambdified(*mesh.triangles.reshape((-1, 9)).T)
            integrated *= 2 * mesh.area_faces
            return integrated

    function, symbols = substitute_barycentric(function)

    b1, b2, x1, x2, x3, y1, y2, y3, z1, z2, z3 = symbols
    # do the first integral for b1
    integrated_1 = sp.integrate(function, b1)
    integrated_1 = (integrated_1.subs({b1: 1 - b2}) -
                    integrated_1.subs({b1: 0}))

    integrated_2 = sp.integrate(integrated_1, b2)
    integrated_2 = (integrated_2.subs({b2: 1}) -
                    integrated_2.subs({b2: 0}))

    lambdified = evaluator(expr=integrated_2,
                           expr_args=[x1, y1, z1, x2, y2, z2, x3, y3, z3])

    return lambdified, integrated_2


def substitute_barycentric(function):
    if util.is_string(function):
        function = sympy_parse(function)
    # barycentric coordinates
    b1, b2 = sp.symbols('b1 b2',
                        real=True,
                        positive=True)
    # vertices of the triangles
    x1, x2, x3, y1, y2, y3, z1, z2, z3 = sp.symbols(
        'x1,x2,x3,y1,y2,y3,z1,z2,z3', real=True)

    # generate the substitution dictionary to convert from cartesian to barycentric
    # since the input could have been a sympy expression or a string
    # that we parsed substitute based on name to avoid id(x) issues
    substitutions = {}
    for symbol in function.free_symbols:
        if symbol.name == 'x':
            substitutions[symbol] = b1 * x1 + b2 * x2 + (1 - b1 - b2) * x3
        elif symbol.name == 'y':
            substitutions[symbol] = b1 * y1 + b2 * y2 + (1 - b1 - b2) * y3
        elif symbol.name == 'z':
            substitutions[symbol] = b1 * z1 + b2 * z2 + (1 - b1 - b2) * z3
    # apply the conversion to barycentric
    function = function.subs(substitutions)
    log.debug('converted function to barycentric: %s', str(function))

    symbols = (b1, b2, x1, x2, x3, y1, y2, y3, z1, z2, z3)

    return function, symbols


# trimesh/intersections.py

"""
intersections.py
------------------

Primarily mesh-plane intersections (slicing).
"""
import numpy as np

from . import util
from . import geometry
from . import grouping
from . import triangles as tm
from . import transformations as tf

from .constants import tol


def mesh_plane(mesh,
               plane_normal,
               plane_origin,
               return_faces=False,
               local_faces=None,
               cached_dots=None):
    """
    Find a the intersections between a mesh and a plane,
    returning a set of line segments on that plane.

    Parameters
    ---------
    mesh : Trimesh object
      Source mesh to slice
    plane_normal : (3,) float
      Normal vector of plane to intersect with mesh
    plane_origin : (3,) float
      Point on plane to intersect with mesh
    return_faces : bool
      If True return face index each line is from
    local_faces : None or (m,) int
      Limit section to just these faces.
    cached_dots : (n, 3) float
      If an external function has stored dot
      products pass them here to avoid recomputing.

    Returns
    ----------
    lines :  (m, 2, 3) float
      List of 3D line segments in space.
    face_index : (m,) int
      Index of mesh.faces for each line
      Only returned if return_faces was True
    """

    def triangle_cases(signs):
        """
        Figure out which faces correspond to which intersection
        case from the signs of the dot product of each vertex.
        Does this by bitbang each row of signs into an 8 bit
        integer.

        code : signs      : intersects
        0    : [-1 -1 -1] : No
        2    : [-1 -1  0] : No
        4    : [-1 -1  1] : Yes; 2 on one side, 1 on the other
        6    : [-1  0  0] : Yes; one edge fully on plane
        8    : [-1  0  1] : Yes; one vertex on plane 2 on different sides
        12   : [-1  1  1] : Yes; 2 on one side, 1 on the other
        14   : [0 0 0]    : No (on plane fully)
        16   : [0 0 1]    : Yes; one edge fully on plane
        20   : [0 1 1]    : No
        28   : [1 1 1]    : No

        Parameters
        ----------
        signs: (n,3) int, all values are -1,0, or 1
          Each row contains the dot product of all three vertices
          in a face with respect to the plane

        Returns
        ---------
        basic : (n,) bool
          Which faces are in the basic intersection case
        one_vertex : (n,) bool
          Which faces are in the one vertex case
        one_edge : (n,) bool
          Which faces are in the one edge case
        """

        signs_sorted = np.sort(signs, axis=1)
        coded = np.zeros(len(signs_sorted), dtype=np.int8) + 14
        for i in range(3):
            coded += signs_sorted[:, i] << 3 - i

        # one edge fully on the plane
        # note that we are only accepting *one* of the on- edge cases,
        # where the other vertex has a positive dot product (16) instead
        # of both on- edge cases ([6, 16])
        # this is so that for regions that are co-planar with the the section plane
        # we don't end up with an invalid boundary
        key = np.zeros(29, dtype=bool)
        key[16] = True
        one_edge = key[coded]

        # one vertex on plane, other two on different sides
        key[:] = False
        key[8] = True
        one_vertex = key[coded]

        # one vertex on one side of the plane, two on the other
        key[:] = False
        key[[4, 12]] = True
        basic = key[coded]

        return basic, one_vertex, one_edge

    def handle_on_vertex(signs, faces, vertices):
        # case where one vertex is on plane
        # and two are on different sides
        vertex_plane = faces[signs == 0]
        edge_thru = faces[signs != 0].reshape((-1, 2))
        point_intersect, valid = plane_lines(
            plane_origin,
            plane_normal,
            vertices[edge_thru.T],
            line_segments=False)
        lines = np.column_stack((
            vertices[vertex_plane[valid]],
            point_intersect)).reshape((-1, 2, 3))
        return lines

    def handle_on_edge(signs, faces, vertices):
        # case where two vertices are on the plane and one is off
        edges = faces[signs == 0].reshape((-1, 2))
        points = vertices[edges]
        return points

    def handle_basic(signs, faces, vertices):
        # case where one vertex is on one side and two are on the other
        unique_element = grouping.unique_value_in_row(
            signs, unique=[-1, 1])
        edges = np.column_stack(
            (faces[unique_element],
             faces[np.roll(unique_element, 1, axis=1)],
             faces[unique_element],
             faces[np.roll(unique_element, 2, axis=1)])).reshape(
            (-1, 2))
        intersections, valid = plane_lines(plane_origin,
                                           plane_normal,
                                           vertices[edges.T],
                                           line_segments=False)
        # since the data has been pre- culled, any invalid intersections at all
        # means the culling was done incorrectly and thus things are broken
        assert valid.all()
        return intersections.reshape((-1, 2, 3))

    # check input plane
    plane_normal = np.asanyarray(plane_normal, dtype=np.float64)
    plane_origin = np.asanyarray(plane_origin, dtype=np.float64)
    if plane_origin.shape != (3,) or plane_normal.shape != (3,):
        raise ValueError('Plane origin and normal must be (3,)!')

    if local_faces is None:
        # do a cross section against all faces
        faces = mesh.faces
    else:
        local_faces = np.asanyarray(
            local_faces, dtype=np.int64)
        # only take the subset of faces if passed
        faces = mesh.faces[local_faces]

    if cached_dots is not None:
        dots = cached_dots
    else:
        # dot product of each vertex with the plane normal indexed by face
        # so for each face the dot product of each vertex is a row
        # shape is the same as mesh.faces (n,3)
        dots = np.dot(mesh.vertices - plane_origin, plane_normal)

    # sign of the dot product is -1, 0, or 1
    # shape is the same as mesh.faces (n,3)
    signs = np.zeros(len(mesh.vertices), dtype=np.int8)
    signs[dots < -tol.merge] = -1
    signs[dots > tol.merge] = 1
    signs = signs[faces]

    # figure out which triangles are in the cross section,
    # and which of the three intersection cases they are in
    cases = triangle_cases(signs)
    # handlers for each case
    handlers = (handle_basic,
                handle_on_vertex,
                handle_on_edge)

    # the (m, 2, 3) line segments
    lines = np.vstack([h(signs[c],
                         faces[c],
                         mesh.vertices)
                       for c, h in zip(cases, handlers)])

    if return_faces:
        # everything that hit something
        index = np.hstack([np.nonzero(c)[0] for c in cases])
        assert index.dtype.kind == 'i'
        if local_faces is None:
            return lines, index
        # we are considering a subset of faces
        # so we need to take the indexes from original
        return lines, local_faces[index]
    return lines


def mesh_multiplane(
        mesh,
        plane_origin,
        plane_normal,
        heights):
    """
    A utility function for slicing a mesh by multiple
    parallel planes which caches the dot product operation.

    Parameters
    -------------
    mesh : trimesh.Trimesh
        Geometry to be sliced by planes
    plane_origin : (3,) float
        Point on a plane
    plane_normal : (3,) float
        Normal vector of plane
    heights : (m,) float
      Offset distances from plane to slice at:
      at `height=0` it will be exactly on the passed plane.

    Returns
    --------------
    lines : (m,) sequence of (n, 2, 2) float
        Lines in space for m planes
    to_3D : (m, 4, 4) float
        Transform to move each section back to 3D
    face_index : (m,) sequence of (n,) int
        Indexes of mesh.faces for each segment
    """
    # check input plane
    plane_normal = util.unitize(plane_normal)
    plane_origin = np.asanyarray(plane_origin,
                                 dtype=np.float64)
    heights = np.asanyarray(heights, dtype=np.float64)

    # dot product of every vertex with plane
    vertex_dots = np.dot(
        plane_normal,
        (mesh.vertices - plane_origin).T)

    # reconstruct transforms for each 2D section
    base_transform = geometry.plane_transform(
        origin=plane_origin,
        normal=plane_normal)
    base_transform = np.linalg.inv(base_transform)

    # alter translation Z inside loop
    translation = np.eye(4)

    # store results
    transforms = []
    face_index = []
    segments = []

    # loop through user specified heights
    for height in heights:
        # offset the origin by the height
        new_origin = plane_origin + (plane_normal * height)
        # offset the dot products by height and index by faces
        new_dots = vertex_dots - height
        # run the intersection with the cached dot products
        lines, index = mesh_plane(
            mesh=mesh,
            plane_origin=new_origin,
            plane_normal=plane_normal,
            return_faces=True,
            cached_dots=new_dots)

        # get the transforms to 3D space and back
        translation[2, 3] = height
        to_3D = np.dot(base_transform, translation)
        to_2D = np.linalg.inv(to_3D)
        transforms.append(to_3D)

        # transform points to 2D frame
        lines_2D = tf.transform_points(
            lines.reshape((-1, 3)), to_2D)

        # if we didn't screw up the transform all
        # of the Z values should be zero
        # assert np.allclose(lines_2D[:, 2], 0.0)

        # reshape back in to lines and discard Z
        lines_2D = lines_2D[:, :2].reshape((-1, 2, 2))
        # store (n, 2, 2) float lines
        segments.append(lines_2D)
        # store (n,) int indexes of mesh.faces
        face_index.append(index)

    # (n, 4, 4) transforms from 2D to 3D
    transforms = np.array(transforms, dtype=np.float64)

    return segments, transforms, face_index


def plane_lines(plane_origin,
                plane_normal,
                endpoints,
                line_segments=True):
    """
    Calculate plane-line intersections

    Parameters
    ---------
    plane_origin : (3,) float
        Point on plane
    plane_normal : (3,) float
        Plane normal vector
    endpoints : (2, n, 3) float
        Points defining lines to be tested
    line_segments : bool
        If True, only returns intersections as valid if
        vertices from endpoints are on different sides
        of the plane.

    Returns
    ---------
    intersections : (m, 3) float
        Cartesian intersection points
    valid : (n, 3) bool
        Indicate whether a valid intersection exists
        for each input line segment
    """
    endpoints = np.asanyarray(endpoints)
    plane_origin = np.asanyarray(plane_origin).reshape(3)
    line_dir = util.unitize(endpoints[1] - endpoints[0])
    plane_normal = util.unitize(np.asanyarray(plane_normal).reshape(3))

    t = np.dot(plane_normal, (plane_origin - endpoints[0]).T)
    b = np.dot(plane_normal, line_dir.T)

    # If the plane normal and line direction are perpendicular, it means
    # the vector is 'on plane', and there isn't a valid intersection.
    # We discard on-plane vectors by checking that the dot product is nonzero
    valid = np.abs(b) > tol.zero
    if line_segments:
        test = np.dot(plane_normal,
                      np.transpose(plane_origin - endpoints[1]))
        different_sides = np.sign(t) != np.sign(test)
        nonzero = np.logical_or(np.abs(t) > tol.zero,
                                np.abs(test) > tol.zero)
        valid = np.logical_and(valid, different_sides)
        valid = np.logical_and(valid, nonzero)

    d = np.divide(t[valid], b[valid])
    intersection = endpoints[0][valid]
    intersection = intersection + np.reshape(d, (-1, 1)) * line_dir[valid]

    return intersection, valid


def planes_lines(plane_origins,
                 plane_normals,
                 line_origins,
                 line_directions,
                 return_distance=False,
                 return_denom=False):
    """
    Given one line per plane find the intersection points.

    Parameters
    -----------
    plane_origins : (n,3) float
        Point on each plane
    plane_normals : (n,3) float
        Normal vector of each plane
    line_origins : (n,3) float
        Point at origin of each line
    line_directions : (n,3) float
        Direction vector of each line
    return_distance : bool
      Return distance from origin to point also
    return_denom : bool
      Return denominator, so you can check for small values

    Returns
    ----------
    on_plane : (n,3) float
      Points on specified planes
    valid : (n,) bool
      Did plane intersect line or not
    distance : (n,) float
      [OPTIONAL] Distance from point
    denom : (n,) float
      [OPTIONAL] Denominator
    """

    # check input types
    plane_origins = np.asanyarray(plane_origins, dtype=np.float64)
    plane_normals = np.asanyarray(plane_normals, dtype=np.float64)
    line_origins = np.asanyarray(line_origins, dtype=np.float64)
    line_directions = np.asanyarray(line_directions, dtype=np.float64)

    # vector from line to plane
    origin_vectors = plane_origins - line_origins

    projection_ori = util.diagonal_dot(origin_vectors, plane_normals)
    projection_dir = util.diagonal_dot(line_directions, plane_normals)

    valid = np.abs(projection_dir) > 1e-5

    distance = np.divide(projection_ori[valid],
                         projection_dir[valid])

    on_plane = line_directions[valid] * distance.reshape((-1, 1))
    on_plane += line_origins[valid]

    result = [on_plane, valid]

    if return_distance:
        result.append(distance)
    if return_denom:
        result.append(projection_dir)

    return result


def slice_faces_plane(vertices,
                      faces,
                      plane_normal,
                      plane_origin,
                      face_index=None,
                      cached_dots=None):
    """
    Slice a mesh (given as a set of faces and vertices) with a plane, returning a
    new mesh (again as a set of faces and vertices) that is the
    portion of the original mesh to the positive normal side of the plane.

    Parameters
    ---------
    vertices : (n, 3) float
        Vertices of source mesh to slice
    faces : (n, 3) int
        Faces of source mesh to slice
    plane_normal : (3,) float
        Normal vector of plane to intersect with mesh
    plane_origin :  (3,) float
        Point on plane to intersect with mesh
    face_index : ((m,) int)
        Indexes of faces to slice. When no mask is provided, the
        default is to slice all faces.
    cached_dots : (n, 3) float
        If an external function has stored dot
        products pass them here to avoid recomputing

    Returns
    ----------
    new_vertices : (n, 3) float
        Vertices of sliced mesh
    new_faces : (n, 3) int
        Faces of sliced mesh
    """

    if len(vertices) == 0:
        return vertices, faces

    # Construct a mask for the faces to slice.
    if face_index is not None:
        faces = faces[face_index]

    if cached_dots is not None:
        dots = cached_dots
    else:
        # dot product of each vertex with the plane normal indexed by face
        # so for each face the dot product of each vertex is a row
        # shape is the same as faces (n,3)
        dots = np.dot(vertices - plane_origin, plane_normal)

    # Find vertex orientations w.r.t. faces for all triangles:
    #  -1 -> vertex "inside" plane (positive normal direction)
    #   0 -> vertex on plane
    #   1 -> vertex "outside" plane (negative normal direction)
    signs = np.zeros(len(vertices), dtype=np.int8)
    signs[dots < -tol.merge] = 1
    signs[dots > tol.merge] = -1
    signs = signs[faces]

    # Find all triangles that intersect this plane
    # onedge <- indices of all triangles intersecting the plane
    # inside <- indices of all triangles "inside" the plane (positive normal)
    signs_sum = signs.sum(axis=1, dtype=np.int8)
    signs_asum = np.abs(signs).sum(axis=1, dtype=np.int8)

    # Cases:
    # (0,0,0),  (-1,0,0),  (-1,-1,0), (-1,-1,-1) <- inside
    # (1,0,0),  (1,1,0),   (1,1,1)               <- outside
    # (1,0,-1), (1,-1,-1), (1,1,-1)              <- onedge
    onedge = np.logical_and(
        signs_asum >= 2,
        np.abs(signs_sum) <= 1)

    inside = signs_sum == -signs_asum

    # for any faces that lie exactly on-the-plane
    # we want to only include them if their normal
    # is backwards from the slicing normal
    on_plane = signs_asum == 0
    if on_plane.any():
        # compute the normals and whether
        # face is degenerate here
        check, valid = tm.normals(vertices[faces[on_plane]])
        # only include faces back from normal
        dot_check = np.dot(check, plane_normal)
        # exclude any degenerate faces from the result
        inside[on_plane] = valid
        # exclude the degenerate face from our mask
        on_plane[on_plane] = valid
        # apply results for this subset
        inside[on_plane] = dot_check < 0.0

    # Automatically include all faces that are "inside"
    new_faces = faces[inside]

    # Separate faces on the edge into two cases: those which will become
    # quads (two vertices inside plane) and those which will become triangles
    # (one vertex inside plane)
    triangles = vertices[faces]
    cut_triangles = triangles[onedge]
    cut_faces_quad = faces[np.logical_and(onedge, signs_sum < 0)]
    cut_faces_tri = faces[np.logical_and(onedge, signs_sum >= 0)]
    cut_signs_quad = signs[np.logical_and(onedge, signs_sum < 0)]
    cut_signs_tri = signs[np.logical_and(onedge, signs_sum >= 0)]

    # If no faces to cut, the surface is not in contact with this plane.
    # Thus, return a mesh with only the inside faces
    if len(cut_faces_quad) + len(cut_faces_tri) == 0:

        if len(new_faces) == 0:
            # if no new faces at all return empty arrays
            empty = (np.zeros((0, 3), dtype=np.float64),
                     np.zeros((0, 3), dtype=np.int64))
            return empty

        # find the unique indices in the new faces
        # using an integer-only unique function
        unique, inverse = grouping.unique_bincount(new_faces.reshape(-1),
                                                   minlength=len(vertices),
                                                   return_inverse=True)

        # use the unique indices for our final vertices and faces
        final_vert = vertices[unique]
        final_face = inverse.reshape((-1, 3))

        return final_vert, final_face

    # Extract the intersections of each triangle's edges with the plane
    o = cut_triangles                               # origins
    d = np.roll(o, -1, axis=1) - o                  # directions
    num = (plane_origin - o).dot(plane_normal)      # compute num/denom
    denom = np.dot(d, plane_normal)
    denom[denom == 0.0] = 1e-12                     # prevent division by zero
    dist = np.divide(num, denom)
    # intersection points for each segment
    int_points = np.einsum('ij,ijk->ijk', dist, d) + o

    # Initialize the array of new vertices with the current vertices
    new_vertices = vertices

    # Handle the case where a new quad is formed by the intersection
    # First, extract the intersection points belonging to a new quad
    quad_int_points = int_points[(signs_sum < 0)[onedge], :, :]
    num_quads = len(quad_int_points)
    if num_quads > 0:
        # Extract the vertex on the outside of the plane, then get the vertices
        # (in CCW order of the inside vertices)
        quad_int_inds = np.where(cut_signs_quad == 1)[1]
        quad_int_verts = cut_faces_quad[
            np.stack((range(num_quads), range(num_quads)), axis=1),
            np.stack(((quad_int_inds + 1) % 3, (quad_int_inds + 2) % 3), axis=1)]

        # Fill out new quad faces with the intersection points as vertices
        new_quad_faces = np.append(
            quad_int_verts,
            np.arange(len(new_vertices),
                      len(new_vertices) +
                      2 * num_quads).reshape(num_quads, 2), axis=1)

        # Extract correct intersection points from int_points and order them in
        # the same way as they were added to faces
        new_quad_vertices = quad_int_points[
            np.stack((range(num_quads), range(num_quads)), axis=1),
            np.stack((((quad_int_inds + 2) % 3).T, quad_int_inds.T),
                     axis=1), :].reshape(2 * num_quads, 3)

        # Add new vertices to existing vertices, triangulate quads, and add the
        # resulting triangles to the new faces
        new_vertices = np.append(new_vertices, new_quad_vertices, axis=0)
        new_tri_faces_from_quads = geometry.triangulate_quads(
            new_quad_faces)
        new_faces = np.append(new_faces, new_tri_faces_from_quads, axis=0)

    # Handle the case where a new triangle is formed by the intersection
    # First, extract the intersection points belonging to a new triangle
    tri_int_points = int_points[(signs_sum >= 0)[onedge], :, :]
    num_tris = len(tri_int_points)
    if num_tris > 0:
        # Extract the single vertex for each triangle inside the plane and get the
        # inside vertices (CCW order)
        tri_int_inds = np.where(cut_signs_tri == -1)[1]
        tri_int_verts = cut_faces_tri[range(
            num_tris), tri_int_inds].reshape(num_tris, 1)

        # Fill out new triangles with the intersection points as vertices
        new_tri_faces = np.append(
            tri_int_verts,
            np.arange(len(new_vertices),
                      len(new_vertices) +
                      2 * num_tris).reshape(num_tris, 2),
            axis=1)

        # Extract correct intersection points and order them in the same way as
        # the vertices were added to the faces
        new_tri_vertices = tri_int_points[
            np.stack((range(num_tris), range(num_tris)), axis=1),
            np.stack((tri_int_inds.T, ((tri_int_inds + 2) % 3).T),
                     axis=1),
            :].reshape(2 * num_tris, 3)

        # Append new vertices and new faces
        new_vertices = np.append(new_vertices, new_tri_vertices, axis=0)
        new_faces = np.append(new_faces, new_tri_faces, axis=0)

    # find the unique indices in the new faces
    # using an integer-only unique function
    unique, inverse = grouping.unique_bincount(new_faces.reshape(-1),
                                               minlength=len(new_vertices),
                                               return_inverse=True)

    # use the unique indexes for our final vertex and faces
    final_vert = new_vertices[unique]
    final_face = inverse.reshape((-1, 3))

    return final_vert, final_face


def slice_mesh_plane(mesh,
                     plane_normal,
                     plane_origin,
                     face_index=None,
                     cap=False,
                     cached_dots=None,
                     engine=None,
                     **kwargs):
    """
    Slice a mesh with a plane returning a new mesh that is the
    portion of the original mesh to the positive normal side
    of the plane.

    Parameters
    ---------
    mesh : Trimesh object
      Source mesh to slice
    plane_normal : (3,) float
      Normal vector of plane to intersect with mesh
    plane_origin :  (3,) float
      Point on plane to intersect with mesh
    cap : bool
      If True, cap the result with a triangulated polygon
    face_index : ((m,) int)
      Indexes of mesh.faces to slice. When no mask is provided, the
      default is to slice all faces.
    cached_dots : (n, 3) float
      If an external function has stored dot
      products pass them here to avoid recomputing
    engine : None or str
      Triangulation engine passed to `triangulate_polygon`
    kwargs : dict
      Passed to the newly created sliced mesh

    Returns
    ----------
    new_mesh : Trimesh object
      Sliced mesh
    """
    # check input for none
    if mesh is None:
        return None

    # avoid circular import
    from .base import Trimesh
    from .path import polygons
    from scipy.spatial import cKDTree
    from .creation import triangulate_polygon

    # check input plane
    plane_normal = np.asanyarray(
        plane_normal, dtype=np.float64)
    plane_origin = np.asanyarray(
        plane_origin, dtype=np.float64)

    # check to make sure origins and normals have acceptable shape
    shape_ok = ((plane_origin.shape == (3,) or
                 util.is_shape(plane_origin, (-1, 3))) and
                (plane_normal.shape == (3,) or
                 util.is_shape(plane_normal, (-1, 3))) and
                plane_origin.shape == plane_normal.shape)
    if not shape_ok:
        raise ValueError('plane origins and normals must be (n, 3)!')

    # start with copy of original mesh, faces, and vertices
    vertices = mesh.vertices.copy()
    faces = mesh.faces.copy()

    if 'process' not in kwargs:
        kwargs['process'] = False

    # slice away specified planes
    for origin, normal in zip(plane_origin.reshape((-1, 3)),
                              plane_normal.reshape((-1, 3))):
        # save the new vertices and faces
        vertices, faces = slice_faces_plane(
            vertices=vertices,
            faces=faces,
            plane_normal=normal,
            plane_origin=origin,
            face_index=face_index)
        # check if cap arg specified
        if cap:
            if face_index:
                # This hasn't been implemented yet.
                raise NotImplementedError(
                    "face_index and cap can't be used together")

            # start by deduplicating vertices again
            unique, inverse = grouping.unique_rows(vertices)
            vertices = vertices[unique]
            # will collect additional faces
            f = inverse[faces]
            # remove degenerate faces by checking to make sure
            # that each face has three unique indices
            f = f[(f[:, :1] != f[:, 1:]).all(axis=1)]
            # transform to the cap plane
            to_2D = geometry.plane_transform(
                origin=origin,
                normal=-normal)
            to_3D = np.linalg.inv(to_2D)

            vertices_2D = tf.transform_points(vertices, to_2D)
            edges = geometry.faces_to_edges(f)
            edges.sort(axis=1)

            on_plane = np.abs(vertices_2D[:, 2]) < 1e-8
            edges = edges[on_plane[edges].all(axis=1)]
            edges = edges[edges[:, 0] != edges[:, 1]]

            unique_edge = grouping.group_rows(
                edges, require_count=1)
            if len(unique) < 3:
                continue

            tree = cKDTree(vertices)
            # collect new faces
            faces = [f]
            for p in polygons.edges_to_polygons(
                    edges[unique_edge], vertices_2D[:, :2]):
                vn, fn = triangulate_polygon(p, engine=engine)
                # collect the original index for the new vertices
                vn3 = tf.transform_points(util.stack_3D(vn), to_3D)
                distance, vid = tree.query(vn3)
                if distance.max() > 1e-8:
                    util.log.debug('triangulate may have inserted vertex!')
                # triangulation should not have inserted vertices
                faces.append(vid[fn])
            faces = np.vstack(faces)

    # return the sliced mesh
    return Trimesh(vertices=vertices, faces=faces, **kwargs)


# trimesh/interval.py

"""
interval.py
--------------

Deal with 1D intervals which are defined by:
  [start position, end position]
"""

import numpy as np


def check(a, b, digits):
    """
    Check input ranges, convert them to vector form,
    and get a fixed precision integer version of them.

    Parameters
    --------------
    a : (2, ) or (2, n) float
      Start and end of a 1D interval
    b : (2, ) or (2, n) float
      Start and end of a 1D interval
    digits : int
      How many digits to consider

    Returns
    --------------
    a : (2, n) float
      Ranges as vector
    b : (2, n) float
      Ranges as vector
    a_int : (2, n) int64
      Ranges rounded to digits, as vector
    b_int : (2, n) int64
      Ranges rounded to digits, as vector
    is_1D : bool
      If True, input was single pair of ranges
    """
    a = np.array(a, dtype=np.float64)
    b = np.array(b, dtype=np.float64)

    if a.shape != b.shape or a.shape[-1] != 2:
        raise ValueError('ranges must be identical and (2,)!')

    # if input was single interval reshape it here
    is_1D = False
    if len(a.shape) == 1:
        a = a.reshape((-1, 2))
        b = b.reshape((-1, 2))
        is_1D = True

    # make sure ranges are sorted
    a.sort(axis=1)
    b.sort(axis=1)

    # compare in fixed point as integers
    a_int = (a * 10**digits).round().astype(np.int64)
    b_int = (b * 10**digits).round().astype(np.int64)

    return a, b, a_int, b_int, is_1D


def intersection(a, b, digits=8):
    """
    Given a pair of ranges, merge them in to
    one range if they overlap at all

    Parameters
    --------------
    a : (2, ) float
      Start and end of a 1D interval
    b : (2, ) float
      Start and end of a 1D interval
    digits : int
      How many digits to consider

    Returns
    --------------
    intersects : bool or (n,) bool
      Indicates if the ranges overlap at all
    new_range : (2, ) or (2, 2) float
      The unioned range from the two inputs,
      or both of the original ranges if not overlapping
    """
    # check shape and convert
    a, b, a_int, b_int, is_1D = check(a, b, digits)

    # what are the starting and ending points of the overlap
    overlap = np.zeros(a.shape, dtype=np.float64)

    # A fully overlaps B
    current = np.logical_and(a_int[:, 0] <= b_int[:, 0],
                             a_int[:, 1] >= b_int[:, 1])
    overlap[current] = b[current]

    # B fully overlaps A
    current = np.logical_and(a_int[:, 0] >= b_int[:, 0],
                             a_int[:, 1] <= b_int[:, 1])
    overlap[current] = a[current]

    # A starts B ends
    # A:, 0   B:, 0     A:, 1        B:, 1
    current = np.logical_and(
        np.logical_and(a_int[:, 0] <= b_int[:, 0],
                       b_int[:, 0] < a_int[:, 1]),
        a_int[:, 1] < b_int[:, 1])
    overlap[current] = np.column_stack([b[current][:, 0],
                                        a[current][:, 1]])

    # B starts A ends
    # B:, 0  A:, 0    B:, 1  A:, 1
    current = np.logical_and(
        np.logical_and(b_int[:, 0] <= a_int[:, 0],
                       a_int[:, 0] < b_int[:, 1]),
        b_int[:, 1] < a_int[:, 1])
    overlap[current] = np.column_stack([a[current][:, 0],
                                        b[current][:, 1]])

    # is range overlapping at all
    intersects = overlap.ptp(axis=1) > 10**-digits

    if is_1D:
        return intersects[0], overlap[0]

    return intersects, overlap


# trimesh/nsphere.py

"""
nsphere.py
--------------

Functions for fitting and minimizing nspheres:
circles, spheres, hyperspheres, etc.
"""
import numpy as np

from . import util
from . import convex

from .constants import log, tol

try:
    # scipy is a soft dependency
    from scipy import spatial
    from scipy.optimize import leastsq
except BaseException as E:
    # raise the exception when someone tries to use it
    from . import exceptions
    leastsq = exceptions.ExceptionWrapper(E)
    spatial = exceptions.ExceptionWrapper(E)

try:
    import psutil

    def _MAX_MEMORY():
        # if we have psutil check actual free memory when called
        return psutil.virtual_memory().free / 2.0
except BaseException:
    def _MAX_MEMORY():
        # use a hardcoded best guess estimate
        return 1e9


def minimum_nsphere(obj):
    """
    Compute the minimum n- sphere for a mesh or a set of points.

    Uses the fact that the minimum n- sphere will be centered at one of
    the vertices of the furthest site voronoi diagram, which is n*log(n)
    but should be pretty fast due to using the scipy/qhull implementations
    of convex hulls and voronoi diagrams.

    Parameters
    ----------
    obj : (n, d) float or trimesh.Trimesh
      Points or mesh to find minimum bounding nsphere

    Returns
    ----------
    center : (d,) float
      Center of fitted n- sphere
    radius : float
      Radius of fitted n-sphere
    """

    # reduce the input points or mesh to the vertices of the convex hull
    # since we are computing the furthest site voronoi diagram this reduces
    # the input complexity substantially and returns the same value
    points = convex.hull_points(obj)

    # we are scaling the mesh to a unit cube
    # this used to pass qhull_options 'QbB' to Voronoi however this had a bug somewhere
    # to avoid this we scale to a unit cube ourselves inside this function
    points_origin = points.min(axis=0)
    points_scale = points.ptp(axis=0).min()
    points = (points - points_origin) / points_scale

    # if all of the points are on an n-sphere already the voronoi
    # method will fail so we check a least squares fit before
    # bothering to compute the voronoi diagram
    fit_C, fit_R, fit_E = fit_nsphere(points)
    # return fit radius and center to global scale
    fit_R = (((points - fit_C)**2).sum(axis=1).max() ** .5) * points_scale
    fit_C = (fit_C * points_scale) + points_origin

    if fit_E < 1e-6:
        # points were on an n-sphere so just return fit
        return fit_C, fit_R

    # calculate a furthest site voronoi diagram
    # this will fail if the points are ALL on the surface of
    # the n-sphere but hopefully the least squares check caught those cases
    # , qhull_options='QbB Pp')
    voronoi = spatial.Voronoi(points, furthest_site=True)

    # find the maximum radius^2 point for each of the voronoi vertices
    # this is worst case quite expensive but we have taken
    # convex hull to reduce n for this operation
    # we are doing comparisons on the radius squared then rooting once
    try:
        # cdist is massivly faster than looping or tiling methods
        # although it does create a very large intermediate array
        # first, get an order of magnitude memory size estimate
        # a float64 would be 8 bytes per entry plus overhead
        memory_estimate = len(voronoi.vertices) * len(points) * 9
        if memory_estimate > _MAX_MEMORY():
            raise MemoryError
        radii_2 = spatial.distance.cdist(
            voronoi.vertices, points,
            metric='sqeuclidean').max(axis=1)
    except MemoryError:
        # log the MemoryError
        log.warning('MemoryError: falling back to slower check!')
        # fall back to a potentially very slow list comprehension
        radii_2 = np.array([((points - v) ** 2).sum(axis=1).max()
                            for v in voronoi.vertices])

    # we want the smallest sphere so take the min of the radii
    radii_idx = radii_2.argmin()

    # return voronoi radius and center to global scale
    radius_v = np.sqrt(radii_2[radii_idx]) * points_scale
    center_v = (voronoi.vertices[radii_idx] *
                points_scale) + points_origin

    if radius_v > fit_R:
        return fit_C, fit_R

    return center_v, radius_v


def fit_nsphere(points, prior=None):
    """
    Fit an n-sphere to a set of points using least squares.

    Parameters
    ------------
    points : (n, d) float
      Points in space
    prior : (d,) float
      Best guess for center of nsphere

    Returns
    ---------
    center : (d,) float
      Location of center
    radius : float
      Mean radius across circle
    error : float
      Peak to peak value of deviation from mean radius
    """
    # make sure points are numpy array
    points = np.asanyarray(points, dtype=np.float64)
    # create ones so we can dot instead of using slower sum
    ones = np.ones(points.shape[1])

    def residuals(center):
        # do the axis sum with a dot
        # this gets called a LOT so worth optimizing
        radii_sq = np.dot((points - center) ** 2, ones)
        # residuals are difference between mean
        # use our sum mean vs .mean() as it is slightly faster
        return radii_sq - (radii_sq.sum() / len(radii_sq))

    if prior is None:
        guess = points.mean(axis=0)
    else:
        guess = np.asanyarray(prior)

    center_result, return_code = leastsq(residuals,
                                         guess,
                                         xtol=1e-8)

    if return_code not in [1, 2, 3, 4]:
        raise ValueError('Least square fit failed!')

    radii = util.row_norm(points - center_result)
    radius = radii.mean()
    error = radii.ptp()
    return center_result, radius, error


def is_nsphere(points):
    """
    Check if a list of points is an nsphere.

    Parameters
    -----------
    points : (n, dimension) float
      Points in space

    Returns
    -----------
    check : bool
      True if input points are on an nsphere
    """
    center, radius, error = fit_nsphere(points)
    check = error < tol.merge
    return check


# trimesh/parent.py

"""
parent.py
-------------

The base class for Trimesh, PointCloud, and Scene objects
"""
import abc
import warnings

import numpy as np

from . import caching
from . import transformations as tf
from .util import ABC


class Geometry(ABC):

    """
    `Geometry` is the parent class for all geometry.

    By decorating a method with `abc.abstractmethod` it means
    the objects that inherit from `Geometry` MUST implement
    those methods.
    """

    @abc.abstractproperty
    def bounds(self):
        pass

    @abc.abstractproperty
    def extents(self):
        pass

    @abc.abstractmethod
    def apply_transform(self, matrix):
        pass

    @abc.abstractmethod
    def is_empty(self):
        pass

    def crc(self):
        """
        DEPRECATED OCTOBER 2023 : Use `hash(geometry)`

        Get a hash of the current geometry.

        Returns
        ---------
        hash : int
          Hash of current graph and geometry.
        """
        warnings.warn(
            '`geometry.crc()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def hash(self):
        """
        DEPRECATED OCTOBER 2023 : Use `hash(geometry)`

        Get a hash of the current geometry.

        Returns
        ---------
        hash : int
          Hash of current graph and geometry.
        """
        warnings.warn(
            '`geometry.hash()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def md5(self):
        """
        DEPRECATED OCTOBER 2023 : Use `hash(geometry)`

        Get a hash of the current geometry.

        Returns
        ---------
        hash : int
          Hash of current graph and geometry.
        """
        warnings.warn(
            '`geometry.md5()` is deprecated and will ' +
            'be removed in October 2023: replace ' +
            'with `geometry.__hash__()` or `hash(geometry)`',
            category=DeprecationWarning, stacklevel=2)
        return self.__hash__()

    def __hash__(self):
        """
        Get a hash of the current geometry.

        Returns
        ---------
        hash : int
          Hash of current graph and geometry.
        """
        return self._data.__hash__()

    @abc.abstractmethod
    def copy(self):
        pass

    @abc.abstractmethod
    def show(self):
        pass

    @abc.abstractmethod
    def __add__(self, other):
        pass

    @abc.abstractmethod
    def export(self, file_obj, file_type=None):
        pass

    def __repr__(self):
        """
        Print quick summary of the current geometry without
        computing properties.

        Returns
        -----------
        repr : str
          Human readable quick look at the geometry.
        """
        elements = []
        if hasattr(self, 'vertices'):
            # for Trimesh and PointCloud
            elements.append('vertices.shape={}'.format(
                self.vertices.shape))
        if hasattr(self, 'faces'):
            # for Trimesh
            elements.append('faces.shape={}'.format(
                self.faces.shape))
        if hasattr(self, 'geometry') and isinstance(
                self.geometry, dict):
            # for Scene
            elements.append('len(geometry)={}'.format(
                len(self.geometry)))
        if 'Voxel' in type(self).__name__:
            # for VoxelGrid objects
            elements.append(str(self.shape)[1:-1])
        return '<trimesh.{}({})>'.format(
            type(self).__name__, ', '.join(elements))

    def apply_translation(self, translation):
        """
        Translate the current mesh.

        Parameters
        ----------
        translation : (3,) float
          Translation in XYZ
        """
        translation = np.asanyarray(translation, dtype=np.float64)
        if translation.shape == (2,):
            # create a planar matrix if we were passed a 2D offset
            return self.apply_transform(
                tf.planar_matrix(offset=translation))
        elif translation.shape != (3,):
            raise ValueError('Translation must be (3,) or (2,)!')

        # manually create a translation matrix
        matrix = np.eye(4)
        matrix[:3, 3] = translation
        return self.apply_transform(matrix)

    def apply_scale(self, scaling):
        """
        Scale the mesh.

        Parameters
        ----------
        scaling : float or (3,) float
          Scale factor to apply to the mesh
        """
        matrix = tf.scale_and_translate(scale=scaling)
        # apply_transform will work nicely even on negative scales
        return self.apply_transform(matrix)

    def __radd__(self, other):
        """
        Concatenate the geometry allowing concatenation with
        built in `sum()` function:
          `sum(Iterable[trimesh.Trimesh])`

        Parameters
        ------------
        other : Geometry
          Geometry or 0

        Returns
        ----------
        concat : Geometry
          Geometry of combined result
        """

        if other == 0:
            # adding 0 to a geometry never makes sense
            return self
        # otherwise just use the regular add function
        return self.__add__(type(self)(other))


class Geometry3D(Geometry):

    """
    The `Geometry3D` object is the parent object of geometry objects
    which are three dimensional, including Trimesh, PointCloud,
    and Scene objects.
    """

    @caching.cache_decorator
    def bounding_box(self):
        """
        An axis aligned bounding box for the current mesh.

        Returns
        ----------
        aabb : trimesh.primitives.Box
          Box object with transform and extents defined
          representing the axis aligned bounding box of the mesh
        """
        from . import primitives

        transform = np.eye(4)
        # translate to center of axis aligned bounds
        transform[:3, 3] = self.bounds.mean(axis=0)

        aabb = primitives.Box(transform=transform,
                              extents=self.extents,
                              mutable=False)
        return aabb

    @caching.cache_decorator
    def bounding_box_oriented(self):
        """
        An oriented bounding box for the current mesh.

        Returns
        ---------
        obb : trimesh.primitives.Box
          Box object with transform and extents defined
          representing the minimum volume oriented
          bounding box of the mesh
        """
        from . import primitives, bounds
        to_origin, extents = bounds.oriented_bounds(self)
        obb = primitives.Box(transform=np.linalg.inv(to_origin),
                             extents=extents,
                             mutable=False)
        return obb

    @caching.cache_decorator
    def bounding_sphere(self):
        """
        A minimum volume bounding sphere for the current mesh.

        Note that the Sphere primitive returned has an unpadded
        exact `sphere_radius` so while the distance of every vertex
        of the current mesh from sphere_center will be less than
        sphere_radius, the faceted sphere primitive may not
        contain every vertex.

        Returns
        --------
        minball : trimesh.primitives.Sphere
          Sphere primitive containing current mesh
        """
        from . import primitives, nsphere
        center, radius = nsphere.minimum_nsphere(self)
        minball = primitives.Sphere(center=center,
                                    radius=radius,
                                    mutable=False)
        return minball

    @caching.cache_decorator
    def bounding_cylinder(self):
        """
        A minimum volume bounding cylinder for the current mesh.

        Returns
        --------
        mincyl : trimesh.primitives.Cylinder
          Cylinder primitive containing current mesh
        """
        from . import primitives, bounds
        kwargs = bounds.minimum_cylinder(self)
        mincyl = primitives.Cylinder(mutable=False, **kwargs)
        return mincyl

    @caching.cache_decorator
    def bounding_primitive(self):
        """
        The minimum volume primitive (box, sphere, or cylinder) that
        bounds the mesh.

        Returns
        ---------
        bounding_primitive : object
          Smallest primitive which bounds the mesh:
          trimesh.primitives.Sphere
          trimesh.primitives.Box
          trimesh.primitives.Cylinder
        """
        options = [self.bounding_box_oriented,
                   self.bounding_sphere,
                   self.bounding_cylinder]
        volume_min = np.argmin([i.volume for i in options])
        bounding_primitive = options[volume_min]
        return bounding_primitive

    def apply_obb(self):
        """
        Apply the oriented bounding box transform to the current mesh.

        This will result in a mesh with an AABB centered at the
        origin and the same dimensions as the OBB.

        Returns
        ----------
        matrix : (4, 4) float
          Transformation matrix that was applied
          to mesh to move it into OBB frame
        """
        matrix = self.bounding_box_oriented.primitive.transform
        matrix = np.linalg.inv(matrix)
        self.apply_transform(matrix)
        return matrix


# trimesh/permutate.py

"""
permutate.py
-------------

Randomly deform meshes in different ways.
"""

import numpy as np

from . import transformations
from . import triangles as triangles_module
from . import util


def transform(mesh, translation_scale=1000.0):
    """
    Return a permutated variant of a mesh by randomly reording faces
    and rotatating + translating a mesh by a random matrix.

    Parameters
    ----------
    mesh : trimesh.Trimesh
      Mesh, will not be altered by this function

    Returns
    ----------
    permutated : trimesh.Trimesh
      Mesh with same faces as input mesh but reordered
      and rigidly transformed in space.
    """
    # rotate and translate randomly
    matrix = transformations.random_rotation_matrix(
        translate=translation_scale)

    # randomly re-order triangles
    triangles = np.random.permutation(mesh.triangles).reshape((-1, 3))
    # apply rigid transform
    triangles = transformations.transform_points(triangles, matrix)

    # extract the class from the input object
    mesh_type = util.type_named(mesh, 'Trimesh')
    # generate a new mesh from the permutated data
    permutated = mesh_type(
        **triangles_module.to_kwargs(triangles.reshape((-1, 3, 3))))

    return permutated


def noise(mesh, magnitude=None):
    """
    Add gaussian noise to every vertex of a mesh, making
    no effort to maintain topology or sanity.

    Parameters
    ----------
    mesh : trimesh.Trimesh
      Input geometry, will not be altered
    magnitude : float
      What is the maximum distance per axis we can displace a vertex.
      If None, value defaults to (mesh.scale / 100.0)

    Returns
    ----------
    permutated : trimesh.Trimesh
      Input mesh with noise applied
    """
    if magnitude is None:
        magnitude = mesh.scale / 100.0

    random = (np.random.random(mesh.vertices.shape) - .5) * magnitude
    vertices_noise = mesh.vertices.copy() + random

    # make sure we've re- ordered faces randomly
    triangles = np.random.permutation(vertices_noise[mesh.faces])

    mesh_type = util.type_named(mesh, 'Trimesh')
    permutated = mesh_type(**triangles_module.to_kwargs(triangles))

    return permutated


def tessellation(mesh):
    """
    Subdivide each face of a mesh into three faces with the new vertex
    randomly placed inside the old face.

    This produces a mesh with exactly the same surface area and volume
    but with different tessellation.

    Parameters
    ------------
    mesh : trimesh.Trimesh
      Input geometry

    Returns
    ----------
    permutated : trimesh.Trimesh
      Mesh with remeshed facets
    """
    # create random barycentric coordinates for each face
    # pad all coordinates by a small amount to bias new vertex towards center
    barycentric = np.random.random(mesh.faces.shape) + .05
    barycentric /= barycentric.sum(axis=1).reshape((-1, 1))

    # create one new vertex somewhere in a face
    vertex_face = (barycentric.reshape((-1, 3, 1))
                   * mesh.triangles).sum(axis=1)
    vertex_face_id = np.arange(len(vertex_face)) + len(mesh.vertices)

    # new vertices are the old vertices stacked on the vertices in the faces
    vertices = np.vstack((mesh.vertices, vertex_face))
    # there are three new faces per old face, and we maintain correct winding
    faces = np.vstack((np.column_stack((mesh.faces[:, [0, 1]], vertex_face_id)),
                       np.column_stack(
                           (mesh.faces[:, [1, 2]], vertex_face_id)),
                       np.column_stack((mesh.faces[:, [2, 0]], vertex_face_id))))
    # make sure the order of the faces is permutated
    faces = np.random.permutation(faces)

    mesh_type = util.type_named(mesh, 'Trimesh')
    permutated = mesh_type(vertices=vertices,
                           faces=faces)
    return permutated


class Permutator:

    def __init__(self, mesh):
        """
        A convenience object to get permutated versions of a mesh.
        """
        self._mesh = mesh

    def transform(self, translation_scale=1000):
        return transform(
            self._mesh, translation_scale=translation_scale)

    def noise(self, magnitude=None):
        return noise(self._mesh, magnitude)

    def tessellation(self):
        return tessellation(self._mesh)


try:
    # copy the function docstrings to the helper object
    Permutator.noise.__doc__ = noise.__doc__
    Permutator.transform.__doc__ = transform.__doc__
    Permutator.tessellation.__doc__ = tessellation.__doc__
except AttributeError:
    # no docstrings in Python2
    pass


# trimesh/points.py

"""
points.py
-------------

Functions dealing with (n, d) points.
"""
import copy

import numpy as np

from .parent import Geometry3D
from .geometry import plane_transform
from .constants import tol
from .visual.color import VertexColor


from . import util
from . import caching
from . import grouping
from . import transformations


def point_plane_distance(points,
                         plane_normal,
                         plane_origin=None):
    """
    The minimum perpendicular distance of a point to a plane.

    Parameters
    -----------
    points : (n, 3) float
      Points in space
    plane_normal : (3,) float
      Unit normal vector
    plane_origin : (3,) float
      Plane origin in space

    Returns
    ------------
    distances : (n,) float
      Distance from point to plane
    """
    points = np.asanyarray(points, dtype=np.float64)
    if plane_origin is None:
        w = points
    else:
        w = points - plane_origin
    distances = np.dot(plane_normal, w.T) / np.linalg.norm(plane_normal)
    return distances


def major_axis(points):
    """
    Returns an approximate vector representing the major
    axis of the passed points.

    Parameters
    -------------
    points : (n, dimension) float
      Points in space

    Returns
    -------------
    axis : (dimension,) float
      Vector along approximate major axis
    """
    U, S, V = np.linalg.svd(points)
    axis = util.unitize(np.dot(S, V))
    return axis


def plane_fit(points):
    """
    Fit a plane to points using SVD.

    Parameters
    ---------
    points : (n, 3) float or (p, n, 3,) float
      3D points in space
      Second option allows to simultaneously compute
      p centroids and normals

    Returns
    ---------
    C : (3,) float or (p, 3,) float
      Point on the plane
    N : (3,) float or (p, 3,) float
      Unit normal vector of plane
    """
    # make sure input is numpy array
    points = np.asanyarray(points, dtype=np.float64)
    assert points.ndim == 2 or points.ndim == 3
    # with only one point set, np.dot is faster
    if points.ndim == 2:
        # make the plane origin the mean of the points
        C = points.mean(axis=0)
        # points offset by the plane origin
        x = points - C[None, :]
        # create a (3, 3) matrix
        M = np.dot(x.T, x)
    else:
        # make the plane origin the mean of the points
        C = points.mean(axis=1)
        # points offset by the plane origin
        x = points - C[:, None, :]
        # create a (p, 3, 3) matrix
        M = np.einsum('pnd, pnm->pdm', x, x)
    # run SVD
    N = np.linalg.svd(M)[0][..., -1]
    # return the centroid(s) and normal(s)
    return C, N


def radial_sort(points,
                origin,
                normal,
                start=None):
    """
    Sorts a set of points radially (by angle) around an
    axis specified by origin and normal vector.

    Parameters
    --------------
    points : (n, 3) float
      Points in space
    origin : (3,)  float
      Origin to sort around
    normal : (3,)  float
      Vector to sort around
    start : (3,) float
      Vector to specify start position in counter-clockwise
      order viewing in direction of normal, MUST not be
      parallel with normal

    Returns
    --------------
    ordered : (n, 3) float
      Same as input points but reordered
    """

    # create two axis perpendicular to each other and
    # the normal and project the points onto them
    if start is None:
        axis0 = [normal[0], normal[2], -normal[1]]
        axis1 = np.cross(normal, axis0)
    else:
        normal, start = util.unitize([normal, start])
        if np.abs(1 - np.abs(np.dot(normal, start))) < tol.zero:
            raise ValueError('start must not parallel with normal')
        axis0 = np.cross(start, normal)
        axis1 = np.cross(axis0, normal)
    vectors = points - origin
    # calculate the angles of the points on the axis
    angles = np.arctan2(np.dot(vectors, axis0),
                        np.dot(vectors, axis1))
    # return the points sorted by angle
    return points[angles.argsort()[::-1]]


def project_to_plane(points,
                     plane_normal,
                     plane_origin,
                     transform=None,
                     return_transform=False,
                     return_planar=True):
    """
    Project (n, 3) points onto a plane.

    Parameters
    -----------
    points : (n, 3) float
      Points in space.
    plane_normal : (3,) float
      Unit normal vector of plane
    plane_origin : (3,)
      Origin point of plane
    transform : None or (4, 4) float
      Homogeneous transform, if specified, normal+origin are overridden
    return_transform : bool
      Returns the (4, 4) matrix used or not
    return_planar : bool
      Return (n, 2) points rather than (n, 3) points
    """

    if np.all(np.abs(plane_normal) < tol.zero):
        raise NameError('Normal must be nonzero!')

    if transform is None:
        transform = plane_transform(plane_origin, plane_normal)

    transformed = transformations.transform_points(points, transform)
    transformed = transformed[:, 0:(3 - int(return_planar))]

    if return_transform:
        polygon_to_3D = np.linalg.inv(transform)
        return transformed, polygon_to_3D
    return transformed


def remove_close(points, radius):
    """
    Given an (n, m) array of points return a subset of
    points where no point is closer than radius.

    Parameters
    ------------
    points : (n, dimension) float
      Points in space
    radius : float
      Minimum radius between result points

    Returns
    ------------
    culled : (m, dimension) float
      Points in space
    mask : (n,) bool
      Which points from the original points were returned
    """
    from scipy.spatial import cKDTree

    tree = cKDTree(points)
    # get the index of every pair of points closer than our radius
    pairs = tree.query_pairs(radius, output_type='ndarray')

    # how often each vertex index appears in a pair
    # this is essentially a cheaply computed "vertex degree"
    # in the graph that we could construct for connected points
    count = np.bincount(pairs.ravel(), minlength=len(points))

    # for every pair we know we have to remove one of them
    # which of the two options we pick can have a large impact
    # on how much over-culling we end up doing
    column = count[pairs].argmax(axis=1)

    # take the value in each row with the highest degree
    # there is probably better numpy slicing you could do here
    highest = pairs.ravel()[column + 2 * np.arange(len(column))]

    # mask the vertices by index
    mask = np.ones(len(points), dtype=bool)
    mask[highest] = False

    if tol.strict:
        # verify we actually did what we said we'd do
        test = cKDTree(points[mask])
        assert len(test.query_pairs(radius)) == 0

    return points[mask], mask


def k_means(points, k, **kwargs):
    """
    Find k centroids that attempt to minimize the k- means problem:
    https://en.wikipedia.org/wiki/Metric_k-center

    Parameters
    ----------
    points:  (n, d) float
      Points in space
    k : int
      Number of centroids to compute
    **kwargs : dict
      Passed directly to scipy.cluster.vq.kmeans

    Returns
    ----------
    centroids : (k, d) float
      Points in some space
    labels: (n) int
      Indexes for which points belong to which centroid
    """
    from scipy.cluster.vq import kmeans
    from scipy.spatial import cKDTree

    points = np.asanyarray(points, dtype=np.float64)
    points_std = points.std(axis=0)
    points_std[points_std < tol.zero] = 1
    whitened = points / points_std
    centroids_whitened, distortion = kmeans(whitened, k, **kwargs)
    centroids = centroids_whitened * points_std

    # find which centroid each point is closest to
    tree = cKDTree(centroids)
    labels = tree.query(points, k=1)[1]

    return centroids, labels


def tsp(points, start=0):
    """
    Find an ordering of points where each is visited and
    the next point is the closest in euclidean distance,
    and if there are multiple points with equal distance
    go to an arbitrary one.

    Assumes every point is visitable from every other point,
    i.e. the travelling salesman problem on a fully connected
    graph. It is not a MINIMUM traversal; rather it is a
    "not totally goofy traversal, quickly." On random points
    this traversal is often ~20x shorter than random ordering,
    and executes on 1000 points in around 29ms on a 2014 i7.

    Parameters
    ---------------
    points : (n, dimension) float
      ND points in space
    start : int
      The index of points we should start at

    Returns
    ---------------
    traversal : (n,) int
      Ordered traversal visiting every point
    distances : (n - 1,) float
      The euclidean distance between points in traversal
    """
    # points should be float
    points = np.asanyarray(points, dtype=np.float64)

    if len(points.shape) != 2:
        raise ValueError('points must be (n, dimension)!')

    # start should be an index
    start = int(start)

    # a mask of unvisited points by index
    unvisited = np.ones(len(points), dtype=bool)
    unvisited[start] = False

    # traversal of points by index
    traversal = np.zeros(len(points), dtype=np.int64) - 1
    traversal[0] = start
    # list of distances
    distances = np.zeros(len(points) - 1, dtype=np.float64)
    # a mask of indexes in order
    index_mask = np.arange(len(points), dtype=np.int64)

    # in the loop we want to call distances.sum(axis=1)
    # a lot and it's actually kind of slow for "reasons"
    # dot products with ones is equivalent and ~2x faster
    sum_ones = np.ones(points.shape[1])

    # loop through all points
    for i in range(len(points) - 1):
        # which point are we currently on
        current = points[traversal[i]]

        # do NlogN distance query
        # use dot instead of .sum(axis=1) or np.linalg.norm
        # as it is faster, also don't square root here
        dist = np.dot((points[unvisited] - current) ** 2,
                      sum_ones)

        # minimum distance index
        min_index = dist.argmin()
        # successor is closest unvisited point
        successor = index_mask[unvisited][min_index]
        # update the mask
        unvisited[successor] = False
        # store the index to the traversal
        traversal[i + 1] = successor
        # store the distance
        distances[i] = dist[min_index]

    # we were comparing distance^2 so take square root
    distances **= 0.5

    return traversal, distances


def plot_points(points, show=True):
    """
    Plot an (n, 3) list of points using matplotlib

    Parameters
    -------------
    points : (n, 3) float
      Points in space
    show : bool
      If False, will not show until plt.show() is called
    """
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D  # NOQA

    points = np.asanyarray(points, dtype=np.float64)

    if len(points.shape) != 2:
        raise ValueError('Points must be (n, 2|3)!')

    if points.shape[1] == 3:
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
        ax.scatter(*points.T)
    elif points.shape[1] == 2:
        plt.scatter(*points.T)
    else:
        raise ValueError('points not 2D/3D: {}'.format(
            points.shape))

    if show:
        plt.show()


class PointCloud(Geometry3D):
    """
    Hold 3D points in an object which can be visualized
    in a scene.
    """

    def __init__(self, vertices, colors=None, metadata=None, **kwargs):
        """
        Load an array of points into a PointCloud object.

        Parameters
        -------------
        vertices : (n, 3) float
          Points in space
        colors : (n, 4) uint8 or None
          RGBA colors for each point
        metadata : dict or None
          Metadata about points
        """
        self._data = caching.DataStore()
        self._cache = caching.Cache(self._data.__hash__)
        self.metadata = {}

        if metadata is not None:
            self.metadata.update(metadata)

        # load vertices
        self.vertices = vertices

        if 'vertex_colors' in kwargs and colors is None:
            colors = kwargs['vertex_colors']

        # save visual data to vertex color object
        self.visual = VertexColor(colors=colors, obj=self)

    def __setitem__(self, *args, **kwargs):
        return self.vertices.__setitem__(*args, **kwargs)

    def __getitem__(self, *args, **kwargs):
        return self.vertices.__getitem__(*args, **kwargs)

    @property
    def shape(self):
        """
        Get the shape of the pointcloud

        Returns
        ----------
        shape : (2,) int
          Shape of vertex array
        """
        return self.vertices.shape

    @property
    def is_empty(self):
        """
        Are there any vertices defined or not.

        Returns
        ----------
        empty : bool
          True if no vertices defined
        """
        return len(self.vertices) == 0

    def copy(self):
        """
        Safely get a copy of the current point cloud.

        Copied objects will have emptied caches to avoid memory
        issues and so may be slow on initial operations until
        caches are regenerated.

        Current object will *not* have its cache cleared.

        Returns
        ---------
        copied : trimesh.PointCloud
          Copy of current point cloud
        """
        copied = PointCloud(vertices=None)

        # copy vertex and face data
        copied._data.data = copy.deepcopy(self._data.data)
        # get metadata
        copied.metadata = copy.deepcopy(self.metadata)

        # make sure cache is set from here
        copied._cache.clear()

        return copied

    def hash(self):
        """
        Get a hash of the current vertices.

        Returns
        ----------
        hash : str
          Hash of self.vertices
        """
        return self._data.__hash__()

    def crc(self):
        """
        Get a CRC hash of the current vertices.

        Returns
        ----------
        crc : int
          Hash of self.vertices
        """
        return self._data.crc()

    def merge_vertices(self):
        """
        Merge vertices closer than tol.merge (default: 1e-8)
        """
        # run unique rows
        unique, inverse = grouping.unique_rows(self.vertices)

        # apply unique mask to vertices
        self.vertices = self.vertices[unique]

        # apply unique mask to colors
        if (self.colors is not None and
                len(self.colors) == len(inverse)):
            self.colors = self.colors[unique]

    def apply_transform(self, transform):
        """
        Apply a homogeneous transformation to the PointCloud
        object in- place.

        Parameters
        --------------
        transform : (4, 4) float
          Homogeneous transformation to apply to PointCloud
        """
        self.vertices = transformations.transform_points(
            self.vertices, matrix=transform)
        return self

    @property
    def bounds(self):
        """
        The axis aligned bounds of the PointCloud

        Returns
        ------------
        bounds : (2, 3) float
          Minimum, Maximum verteex
        """
        return np.array([self.vertices.min(axis=0),
                         self.vertices.max(axis=0)])

    @property
    def extents(self):
        """
        The size of the axis aligned bounds

        Returns
        ------------
        extents : (3,) float
          Edge length of axis aligned bounding box
        """
        return self.bounds.ptp(axis=0)

    @property
    def centroid(self):
        """
        The mean vertex position

        Returns
        ------------
        centroid : (3,) float
          Mean vertex position
        """
        return self.vertices.mean(axis=0)

    @property
    def vertices(self):
        """
        Vertices of the PointCloud

        Returns
        ------------
        vertices : (n, 3) float
          Points in the PointCloud
        """
        return self._data['vertices']

    @vertices.setter
    def vertices(self, data):
        if data is None:
            self._data['vertices'] = None
        else:
            # we want to copy data for new object
            data = np.array(data, dtype=np.float64, copy=True)
            if not util.is_shape(data, (-1, 3)):
                raise ValueError('Point clouds must be (n, 3)!')
            self._data['vertices'] = data

    @property
    def colors(self):
        """
        Stored per- point color

        Returns
        ----------
        colors : (len(self.vertices), 4) np.uint8
          Per- point RGBA color
        """
        return self.visual.vertex_colors

    @colors.setter
    def colors(self, data):
        self.visual.vertex_colors = data

    @caching.cache_decorator
    def kdtree(self):
        """
        Return a scipy.spatial.cKDTree of the vertices of the mesh.
        Not cached as this lead to observed memory issues and segfaults.

        Returns
        ---------
        tree : scipy.spatial.cKDTree
          Contains mesh.vertices
        """

        from scipy.spatial import cKDTree
        tree = cKDTree(self.vertices.view(np.ndarray))
        return tree

    @caching.cache_decorator
    def convex_hull(self):
        """
        A convex hull of every point.

        Returns
        -------------
        convex_hull : trimesh.Trimesh
          A watertight mesh of the hull of the points
        """
        from . import convex
        return convex.convex_hull(self.vertices)

    def scene(self):
        """
        A scene containing just the PointCloud

        Returns
        ----------
        scene : trimesh.Scene
          Scene object containing this PointCloud
        """
        from .scene.scene import Scene
        return Scene(self)

    def show(self, **kwargs):
        """
        Open a viewer window displaying the current PointCloud
        """
        self.scene().show(**kwargs)

    def export(self, file_obj=None, file_type=None, **kwargs):
        """
        Export the current pointcloud to a file object.
        If file_obj is a filename, file will be written there.
        Supported formats are xyz
        Parameters
        ------------
        file_obj: open writeable file object
          str, file name where to save the pointcloud
          None, if you would like this function to return the export blob
        file_type: str
          Which file type to export as.
          If file name is passed this is not required
        """
        from .exchange.export import export_mesh
        return export_mesh(self,
                           file_obj=file_obj,
                           file_type=file_type,
                           **kwargs)

    def query(self, input_points, **kwargs):
        """
        Find the the closest points and associated attributes from this PointCloud.
        Parameters
        ------------
        input_points : (n, 3) float
          Input query points
        kwargs : dict
          Arguments for proximity.query_from_points
        result : proximity.NearestQueryResult
            Result of the query.
        """
        from .proximity import query_from_points
        return query_from_points(
            self.vertices, input_points, self.kdtree, **kwargs)

    def __add__(self, other):
        if len(other.colors) == len(self.colors) == 0:
            colors = None
        else:
            # preserve colors
            # if one point cloud has no color property use black
            other_colors = [[0, 0, 0, 255]] * \
                len(other.vertices) if len(other.colors) == 0 else other.colors
            self_colors = [[0, 0, 0, 255]] * \
                len(self.vertices) if len(self.colors) == 0 else self.colors
            colors = np.vstack((self_colors, other_colors))
        return PointCloud(vertices=np.vstack(
            (self.vertices, other.vertices)), colors=colors)


# trimesh/poses.py

"""
poses.py
-----------

Find stable orientations of meshes.
"""
import numpy as np

from .triangles import points_to_barycentric

try:
    import networkx as nx
except BaseException as E:
    # create a dummy module which will raise the ImportError
    # or other exception only when someone tries to use networkx
    from .exceptions import ExceptionWrapper
    nx = ExceptionWrapper(E)


def compute_stable_poses(mesh,
                         center_mass=None,
                         sigma=0.0,
                         n_samples=1,
                         threshold=0.0):
    """
    Computes stable orientations of a mesh and their quasi-static probabilities.

    This method samples the location of the center of mass from a multivariate
    gaussian with the mean at the center of mass, and a covariance
    equal to and identity matrix times sigma, over n_samples.

    For each sample, it computes the stable resting poses of the mesh on a
    a planar workspace and evaluates the probabilities of landing in
    each pose if the object is dropped onto the table randomly.

    This method returns the 4x4 homogeneous transform matrices that place
    the shape against the planar surface with the z-axis pointing upwards
    and a list of the probabilities for each pose.

    The transforms and probabilties that are returned are sorted, with the
    most probable pose first.

    Parameters
    ----------
    mesh : trimesh.Trimesh
      The target mesh
    com : (3,) float
      Rhe object center of mass. If None, this method
      assumes uniform density and watertightness and
      computes a center of mass explicitly
    sigma : float
      Rhe covariance for the multivariate gaussian used
      to sample center of mass locations
    n_samples : int
      The number of samples of the center of mass location
    threshold : float
      The probability value at which to threshold
      returned stable poses

    Returns
    -------
    transforms : (n, 4, 4) float
      The homogeneous matrices that transform the
      object to rest in a stable pose, with the
      new z-axis pointing upwards from the table
      and the object just touching the table.
    probs : (n,) float
      Probability in (0, 1) for each pose
    """

    # save convex hull mesh to avoid a cache check
    cvh = mesh.convex_hull

    if center_mass is None:
        center_mass = mesh.center_mass

    # Sample center of mass, rejecting points outside of conv hull
    sample_coms = []
    while len(sample_coms) < n_samples:
        remaining = n_samples - len(sample_coms)
        coms = np.random.multivariate_normal(center_mass,
                                             sigma * np.eye(3),
                                             remaining)
        for c in coms:
            dots = np.einsum('ij,ij->i',
                             c - cvh.triangles_center,
                             cvh.face_normals)
            if np.all(dots < 0):
                sample_coms.append(c)

    norms_to_probs = {}  # Map from normal to probabilities

    # For each sample, compute the stable poses
    for sample_com in sample_coms:

        # Create toppling digraph
        dg = _create_topple_graph(cvh, sample_com)

        # Propagate probabilities to sink nodes with a breadth-first traversal
        nodes = [n for n in dg.nodes() if dg.in_degree(n) == 0]
        n_iters = 0
        while len(nodes) > 0 and n_iters <= len(mesh.faces):
            new_nodes = []
            for node in nodes:
                if dg.out_degree(node) == 0:
                    continue
                successor = next(iter(dg.successors(node)))
                dg.nodes[successor]['prob'] += dg.nodes[node]['prob']
                dg.nodes[node]['prob'] = 0.0
                new_nodes.append(successor)
            nodes = new_nodes
            n_iters += 1

        # Collect stable poses
        for node in dg.nodes():
            if dg.nodes[node]['prob'] > 0.0:
                normal = cvh.face_normals[node]
                prob = dg.nodes[node]['prob']
                key = tuple(np.around(normal, decimals=3))
                if key in norms_to_probs:
                    norms_to_probs[key]['prob'] += 1.0 / n_samples * prob
                else:
                    norms_to_probs[key] = {
                        'prob': 1.0 / n_samples * prob,
                        'normal': normal
                    }

    transforms = []
    probs = []

    # Filter stable poses
    for key in norms_to_probs:
        prob = norms_to_probs[key]['prob']
        if prob > threshold:
            tf = np.eye(4)

            # Compute a rotation matrix for this stable pose
            z = -1.0 * norms_to_probs[key]['normal']
            x = np.array([-z[1], z[0], 0])
            if np.linalg.norm(x) == 0.0:
                x = np.array([1, 0, 0])
            else:
                x = x / np.linalg.norm(x)
            y = np.cross(z, x)
            y = y / np.linalg.norm(y)
            tf[:3, :3] = np.array([x, y, z])

            # Compute the necessary translation for this stable pose
            m = cvh.copy()
            m.apply_transform(tf)
            z = -m.bounds[0][2]
            tf[:3, 3] = np.array([0, 0, z])

            transforms.append(tf)
            probs.append(prob)

    # Sort the results
    transforms = np.array(transforms)
    probs = np.array(probs)
    inds = np.argsort(-probs)

    return transforms[inds], probs[inds]


def _orient3dfast(plane, pd):
    """
    Performs a fast 3D orientation test.

    Parameters
    ----------
    plane: (3,3) float, three points in space that define a plane
    pd:    (3,)  float, a single point

    Returns
    -------
    result: float, if greater than zero then pd is above the plane through
                   the given three points, if less than zero then pd is below
                   the given plane, and if equal to zero then pd is on the
                   given plane.
    """
    pa, pb, pc = plane
    adx = pa[0] - pd[0]
    bdx = pb[0] - pd[0]
    cdx = pc[0] - pd[0]
    ady = pa[1] - pd[1]
    bdy = pb[1] - pd[1]
    cdy = pc[1] - pd[1]
    adz = pa[2] - pd[2]
    bdz = pb[2] - pd[2]
    cdz = pc[2] - pd[2]

    return (adx * (bdy * cdz - bdz * cdy)
            + bdx * (cdy * adz - cdz * ady)
            + cdx * (ady * bdz - adz * bdy))


def _compute_static_prob(tri, com):
    """
    For an object with the given center of mass, compute
    the probability that the given tri would be the first to hit the
    ground if the object were dropped with a pose chosen uniformly at random.

    Parameters
    ----------
    tri: (3,3) float, the vertices of a triangle
    cm:  (3,) float, the center of mass of the object

    Returns
    -------
    prob: float, the probability in [0,1] for the given triangle
    """
    sv = [(v - com) / np.linalg.norm(v - com) for v in tri]

    # Use L'Huilier's Formula to compute spherical area
    a = np.arccos(min(1, max(-1, np.dot(sv[0], sv[1]))))
    b = np.arccos(min(1, max(-1, np.dot(sv[1], sv[2]))))
    c = np.arccos(min(1, max(-1, np.dot(sv[2], sv[0]))))
    s = (a + b + c) / 2.0

    # Prevents weirdness with arctan
    try:
        return 1.0 / np.pi * np.arctan(np.sqrt(np.tan(s / 2) * np.tan(
            (s - a) / 2) * np.tan((s - b) / 2) * np.tan((s - c) / 2)))
    except BaseException:
        s = s + 1e-8
        return 1.0 / np.pi * np.arctan(np.sqrt(np.tan(s / 2) * np.tan(
            (s - a) / 2) * np.tan((s - b) / 2) * np.tan((s - c) / 2)))


def _create_topple_graph(cvh_mesh, com):
    """
    Constructs a toppling digraph for the given convex hull mesh and
    center of mass.

    Each node n_i in the digraph corresponds to a face f_i of the mesh and is
    labelled with the probability that the mesh will land on f_i if dropped
    randomly. Not all faces are stable, and node n_i has a directed edge to
    node n_j if the object will quasi-statically topple from f_i to f_j if it
    lands on f_i initially.

    This computation is described in detail in
    http://goldberg.berkeley.edu/pubs/eps.pdf.

    Parameters
    ----------
    cvh_mesh : trimesh.Trimesh
      Rhe convex hull of the target shape
    com : (3,) float
      The 3D location of the target shape's center of mass

    Returns
    -------
    graph : networkx.DiGraph
      Graph representing static probabilities and toppling
      order for the convex hull
    """
    adj_graph = nx.Graph()
    topple_graph = nx.DiGraph()

    # Create face adjacency graph
    face_pairs = cvh_mesh.face_adjacency
    edges = cvh_mesh.face_adjacency_edges

    graph_edges = []
    for fp, e in zip(face_pairs, edges):
        verts = cvh_mesh.vertices[e]
        graph_edges.append([fp[0], fp[1], {'verts': verts}])

    adj_graph.add_edges_from(graph_edges)

    # Compute static probabilities of landing on each face
    for i, tri in enumerate(cvh_mesh.triangles):
        prob = _compute_static_prob(tri, com)
        topple_graph.add_node(i, prob=prob)

    # Compute COM projections onto planes of each triangle in cvh_mesh
    proj_dists = np.einsum('ij,ij->i', cvh_mesh.face_normals,
                           com - cvh_mesh.triangles[:, 0])
    proj_coms = com - np.einsum('i,ij->ij', proj_dists, cvh_mesh.face_normals)
    barys = points_to_barycentric(cvh_mesh.triangles, proj_coms)
    unstable_face_indices = np.where(np.any(barys < 0, axis=1))[0]

    # For each unstable face, compute the face it topples to
    for fi in unstable_face_indices:
        proj_com = proj_coms[fi]
        centroid = cvh_mesh.triangles_center[fi]
        norm = cvh_mesh.face_normals[fi]

        for tfi in adj_graph[fi]:
            v1, v2 = adj_graph[fi][tfi]['verts']
            if np.dot(np.cross(v1 - centroid, v2 - centroid), norm) < 0:
                tmp = v2
                v2 = v1
                v1 = tmp
            plane1 = [centroid, v1, v1 + norm]
            plane2 = [centroid, v2 + norm, v2]
            if _orient3dfast(plane1, proj_com) >= 0 and _orient3dfast(
                    plane2, proj_com) >= 0:
                break

        topple_graph.add_edge(fi, tfi)

    return topple_graph


# trimesh/primitives.py

"""
primitives.py
----------------

Subclasses of Trimesh objects that are parameterized as primitives.

Useful because you can move boxes and spheres around
and then use trimesh operations on them at any point.
"""
import numpy as np
import abc

from . import util
from . import sample
from . import caching
from . import inertia
from . import creation
from . import triangles
from . import transformations as tf

from .base import Trimesh
from .constants import log, tol

# immutable identity matrix for checks
_IDENTITY = np.eye(4)
_IDENTITY.flags.writeable = False


class _Primitive(Trimesh):
    """
    Geometric _Primitives which are a subclass of Trimesh.
    Mesh is generated lazily when vertices or faces are requested.
    """

    # ignore superclass copy directives
    __copy__ = None
    __deepcopy__ = None

    def __init__(self):
        # run the Trimesh constructor with any passed in arguments
        super(_Primitive, self).__init__()

        # remove any data
        self._data.clear()
        self._validate = False

        # make sure any cached numpy arrays have
        # set `array.flags.writable = False`
        self._cache.force_immutable = True

    def __repr__(self):
        return '<trimesh.primitives.{}>'.format(
            type(self).__name__)

    @property
    def faces(self):
        stored = self._cache['faces']
        if util.is_shape(stored, (-1, 3)):
            return stored
        self._create_mesh()
        return self._cache['faces']

    @faces.setter
    def faces(self, values):
        log.warning('primitive faces are immutable: not setting!')

    @property
    def vertices(self):
        stored = self._cache['vertices']
        if util.is_shape(stored, (-1, 3)):
            return stored

        self._create_mesh()
        return self._cache['vertices']

    @vertices.setter
    def vertices(self, values):
        if values is not None:
            log.warning('primitive vertices are immutable: not setting!')

    @property
    def face_normals(self):
        # we need to avoid the logic in the superclass that
        # is specific to the data model prioritizing faces
        stored = self._cache['face_normals']
        if util.is_shape(stored, (-1, 3)):
            return stored
        # just calculate if not stored
        unit, valid = triangles.normals(self.triangles)
        normals = np.zeros((len(valid), 3))
        normals[valid] = unit
        # store and return
        self._cache['face_normals'] = normals
        return normals

    @face_normals.setter
    def face_normals(self, values):
        if values is not None:
            log.warning('Primitive face normals are immutable!')

    @property
    def transform(self):
        """
        The transform of the Primitive object.

        Returns
        -------------
        transform : (4, 4) float
          Homogeneous transformation matrix
        """
        return self.primitive.transform

    @abc.abstractmethod
    def to_dict(self):
        """
        Should be implemented by each primitive.
        """
        raise NotImplementedError()

    def copy(self, **kwargs):
        """
        Return a copy of the Primitive object.

        Returns
        -------------
        copied : object
          Copy of current primitive
        """
        # get the constructor arguments
        kwargs.update(self.to_dict())
        # remove the type indicator, i.e. `Cylinder`
        kwargs.pop('kind')
        # create a new object with kwargs
        return type(self)(**kwargs)

    def to_mesh(self, **kwargs):
        """
        Return a copy of the Primitive object as a Trimesh.

        Parameters
        -----------
        kwargs : dict
          Passed to the Trimesh object constructor.

        Returns
        ------------
        mesh : trimesh.Trimesh
          Tessellated version of the primitive.
        """
        result = Trimesh(
            vertices=self.vertices.copy(),
            faces=self.faces.copy(),
            face_normals=self.face_normals.copy(),
            process=kwargs.pop('process', False),
            **kwargs)
        return result

    def apply_transform(self, matrix):
        """
        Apply a transform to the current primitive by
        applying a new transform on top of existing
        `self.primitive.transform`. If the matrix
        contains scaling it will change parameters
        like `radius` or `height` automatically.

        Parameters
        ------------
        matrix: (4, 4) float
          Homogeneous transformation
        """
        matrix = np.asanyarray(
            matrix, order='C', dtype=np.float64)
        if matrix.shape != (4, 4):
            raise ValueError('matrix must be `(4, 4)`!')
        if util.allclose(matrix, _IDENTITY, 1e-8):
            # identity matrix is a no-op
            return self

        prim = self.primitive
        # copy the current transform
        current = prim.transform.copy()
        # see if matrix has scaling from the matrix
        scale = np.linalg.det(matrix[:3, :3]) ** (1.0 / 3.0)

        # the objects we handle re-scaling for
        # note that `Extrusion` is NOT supported
        kinds = (Box, Cylinder, Capsule, Sphere)
        if isinstance(self, kinds) and abs(scale - 1.0) > 1e-8:
            # scale the primitive attributes
            if hasattr(prim, 'height'):
                prim.height *= scale
            if hasattr(prim, 'radius'):
                prim.radius *= scale
            if hasattr(prim, 'extents'):
                prim.extents *= scale
            # scale the translation of the current matrix
            current[:3, 3] *= scale
            # apply new matrix, rescale, translate, current
            updated = util.multi_dot([
                matrix,
                tf.scale_matrix(1.0 / scale),
                current])
        else:
            # without scaling just multiply
            updated = np.dot(matrix, current)

        # make sure matrix is a rigid transform
        if not tf.is_rigid(updated):
            raise ValueError("Couldn't produce rigid transform!")

        # apply the new matrix
        self.primitive.transform = updated

        return self

    def _create_mesh(self):
        raise ValueError('Primitive doesn\'t define mesh creation!')


class _PrimitiveAttributes(object):
    """
    Hold the mutable data which defines a primitive.
    """

    def __init__(self, parent, defaults, kwargs, mutable=True):
        """
        Hold the attributes for a Primitive.

        Parameters
        ------------
        parent : _Primitive
          Parent object reference.
        defaults : dict
          The default values for this primitive type.
        kwargs : dict
          User-passed values, i.e. {'radius': 10.0}
        """
        # store actual data in parent object
        self._data = parent._data
        # default values define the keys
        self._defaults = defaults
        # store a reference to the parent ubject
        self._parent = parent
        # start with a copy of all default objects
        self._data.update(defaults)
        # store whether this data is mutable after creation
        self._mutable = mutable
        # assign the keys passed by the user only if
        # they are a property of this primitive
        for key, default in defaults.items():
            value = kwargs.get(key, None)
            if value is not None:
                # convert passed data into type of defaults
                self._data[key] = util.convert_like(value, default)
        # make sure stored values are immutable after setting
        if not self._mutable:
            self._data.mutable = False

    @property
    def __doc__(self):
        # this is generated dynamically as the format
        # operation can be surprisingly slow and most
        # people never call it
        import pprint
        doc = (
            'Store the attributes of a {name} object.\n\n' +
            'When these values are changed, the mesh geometry will \n' +
            'automatically be updated to reflect the new values.\n\n' +
            'Available properties and their default values are:\n {defaults}' +
            '\n\nExample\n---------------\n' +
            'p = trimesh.primitives.{name}()\n' +
            'p.primitive.radius = 10\n' +
            '\n').format(
            name=self._parent.__class__.__name__,
            defaults=pprint.pformat(
                self._defaults,
                width=-1)[1:-1])
        return doc

    def __getattr__(self, key):
        if key.startswith('_'):
            return super(_PrimitiveAttributes, self).__getattr__(key)
        elif key == 'center':
            # this whole __getattr__ is a little hacky
            return self._data['transform'][:3, 3]
        elif key in self._defaults:
            return util.convert_like(self._data[key],
                                     self._defaults[key])
        raise AttributeError(
            "primitive object has no attribute '{}' ".format(key))

    def __setattr__(self, key, value):
        if key.startswith('_'):
            return super(_PrimitiveAttributes,
                         self).__setattr__(key, value)
        elif key == 'center':
            value = np.array(value, dtype=np.float64)
            transform = np.eye(4)
            transform[:3, 3] = value
            self._data['transform'] = transform
            return
        elif key in self._defaults:
            if self._mutable:
                self._data[key] = util.convert_like(
                    value, self._defaults[key])
            else:
                raise ValueError(
                    'Primitive is configured as immutable! Cannot set attribute!')
        else:
            keys = list(self._defaults.keys())
            raise ValueError(
                'Only default attributes {} can be set!'.format(keys))

    def __dir__(self):
        result = sorted(dir(type(self)) +
                        list(self._defaults.keys()))
        return result


class Cylinder(_Primitive):

    def __init__(self,
                 radius=1.0,
                 height=1.0,
                 transform=None,
                 sections=32,
                 mutable=True):
        """
        Create a Cylinder Primitive, a subclass of Trimesh.

        Parameters
        -------------
        radius : float
          Radius of cylinder
        height : float
          Height of cylinder
        transform : (4, 4) float
          Homogeneous transformation matrix
        sections : int
          Number of facets in circle.
        mutable : bool
          Are extents and transform mutable after creation.
        """
        super(Cylinder, self).__init__()

        defaults = {'height': 10.0,
                    'radius': 1.0,
                    'transform': np.eye(4),
                    'sections': 32}
        self.primitive = _PrimitiveAttributes(
            self,
            defaults=defaults,
            kwargs={'height': height,
                    'radius': radius,
                    'transform': transform,
                    'sections': sections},
            mutable=mutable)

    @caching.cache_decorator
    def volume(self):
        """
        The analytic volume of the cylinder primitive.

        Returns
        ---------
        volume : float
          Volume of the cylinder
        """
        volume = ((np.pi * self.primitive.radius ** 2) *
                  self.primitive.height)
        return volume

    @caching.cache_decorator
    def moment_inertia(self):
        """
        The analytic inertia tensor of the cylinder primitive.

        Returns
        ----------
        tensor: (3, 3) float
          3D inertia tensor
        """

        tensor = inertia.cylinder_inertia(
            mass=self.volume,
            radius=self.primitive.radius,
            height=self.primitive.height,
            transform=self.primitive.transform)
        return tensor

    @caching.cache_decorator
    def direction(self):
        """
        The direction of the cylinder's axis.

        Returns
        --------
        axis: (3,) float, vector along the cylinder axis
        """
        axis = np.dot(self.primitive.transform, [0, 0, 1, 0])[:3]
        return axis

    @property
    def segment(self):
        """
        A line segment which if inflated by cylinder radius
        would represent the cylinder primitive.

        Returns
        -------------
        segment : (2, 3) float
          Points representing a single line segment
        """
        # half the height
        half = self.primitive.height / 2.0
        # apply the transform to the Z- aligned segment
        points = np.dot(
            self.primitive.transform,
            np.transpose([[0, 0, -half, 1],
                          [0, 0, half, 1]])).T[:, :3]
        return points

    def to_dict(self):
        """
        Get a copy of the current Cylinder primitive as
        a JSON-serializable dict that matches the schema
        in `trimesh/resources/schema/cylinder.schema.json`

        Returns
        ----------
        as_dict : dict
          Serializable data for this primitive.
        """
        return {'kind': 'cylinder',
                'transform': self.primitive.transform.tolist(),
                'radius': float(self.primitive.radius),
                'height': float(self.primitive.height), }

    def buffer(self, distance):
        """
        Return a cylinder primitive which covers the source
        cylinder by distance: radius is inflated by distance
        height by twice the distance.

        Parameters
        ------------
        distance : float
          Distance to inflate cylinder radius and height

        Returns
        -------------
        buffered : Cylinder
         Cylinder primitive inflated by distance
        """
        distance = float(distance)
        buffered = Cylinder(
            height=self.primitive.height + distance * 2,
            radius=self.primitive.radius + distance,
            transform=self.primitive.transform.copy())
        return buffered

    def _create_mesh(self):
        log.debug('creating mesh for Cylinder primitive')
        mesh = creation.cylinder(radius=self.primitive.radius,
                                 height=self.primitive.height,
                                 sections=self.primitive.sections,
                                 transform=self.primitive.transform)

        self._cache['vertices'] = mesh.vertices
        self._cache['faces'] = mesh.faces
        self._cache['face_normals'] = mesh.face_normals


class Capsule(_Primitive):

    def __init__(self,
                 radius=1.0,
                 height=10.0,
                 transform=None,
                 sections=32,
                 mutable=True):
        """
        Create a Capsule Primitive, a subclass of Trimesh.

        Parameters
        ----------
        radius : float
          Radius of cylinder
        height : float
          Height of cylinder
        transform : (4, 4) float
          Transformation matrix
        sections : int
          Number of facets in circle
        mutable : bool
          Are extents and transform mutable after creation.
        """
        super(Capsule, self).__init__()

        defaults = {'height': 1.0,
                    'radius': 1.0,
                    'transform': np.eye(4),
                    'sections': 32}
        self.primitive = _PrimitiveAttributes(
            self,
            defaults=defaults,
            kwargs={'height': height,
                    'radius': radius,
                    'transform': transform,
                    'sections': sections},
            mutable=mutable)

    @property
    def transform(self):
        return self.primitive.transform

    def to_dict(self):
        """
        Get a copy of the current Capsule primitive as
        a JSON-serializable dict that matches the schema
        in `trimesh/resources/schema/capsule.schema.json`

        Returns
        ----------
        as_dict : dict
          Serializable data for this primitive.
        """
        return {'kind': 'capsule',
                'transform': self.primitive.transform.tolist(),
                'height': float(self.primitive.height),
                'radius': float(self.primitive.radius)}

    @caching.cache_decorator
    def direction(self):
        """
        The direction of the capsule's axis.

        Returns
        --------
        axis : (3,) float
          Vector along the cylinder axis
        """
        axis = np.dot(self.primitive.transform,
                      [0, 0, 1, 0])[:3]
        return axis

    def _create_mesh(self):
        log.debug('creating mesh for `Capsule` primitive')

        mesh = creation.capsule(radius=self.primitive.radius,
                                height=self.primitive.height)
        mesh.apply_transform(self.primitive.transform)

        self._cache['vertices'] = mesh.vertices
        self._cache['faces'] = mesh.faces
        self._cache['face_normals'] = mesh.face_normals


class Sphere(_Primitive):

    def __init__(self,
                 radius=1.0,
                 center=None,
                 transform=None,
                 subdivisions=3,
                 mutable=True):
        """
        Create a Sphere Primitive, a subclass of Trimesh.

        Parameters
        ----------
        radius : float
          Radius of sphere
        center : None or (3,) float
          Center of sphere.
        transform : None or (4, 4) float
          Full homogeneous transform. Pass `center` OR `transform.
        subdivisions : int
          Number of subdivisions for icosphere.
        mutable : bool
          Are extents and transform mutable after creation.
        """

        super(Sphere, self).__init__()

        defaults = {'radius': 1.0,
                    'transform': np.eye(4),
                    'subdivisions': 3}
        constructor = {'radius': float(radius),
                       'subdivisions': int(subdivisions)}
        # center is a helper method for "transform"
        # since a sphere is rotationally symmetric
        if center is not None:
            if transform is not None:
                raise ValueError(
                    'only one of `center` and `transform` may be passed!')
            translate = np.eye(4)
            translate[:3, 3] = center
            constructor['transform'] = translate
        elif transform is not None:
            constructor['transform'] = transform

        # create the attributes object
        self.primitive = _PrimitiveAttributes(
            self, defaults=defaults, kwargs=constructor, mutable=mutable)

    @property
    def center(self):
        return self.primitive.center

    @center.setter
    def center(self, value):
        self.primitive.center = value

    def to_dict(self):
        """
        Get a copy of the current Sphere primitive as
        a JSON-serializable dict that matches the schema
        in `trimesh/resources/schema/sphere.schema.json`

        Returns
        ----------
        as_dict : dict
          Serializable data for this primitive.
        """
        return {'kind': 'sphere',
                'transform': self.primitive.transform.tolist(),
                'radius': float(self.primitive.radius)}

    @property
    def bounds(self):
        # no docstring so will inherit Trimesh docstring
        # return exact bounds from primitive center and radius (rather than faces)
        # self.extents will also use this information
        bounds = np.array([self.primitive.center - self.primitive.radius,
                           self.primitive.center + self.primitive.radius])
        return bounds

    @property
    def bounding_box_oriented(self):
        # for a sphere the oriented bounding box is the same as the axis aligned
        # bounding box, and a sphere is the absolute slowest case for the OBB calculation
        # as it is a convex surface with a ton of face normals that all need to
        # be checked
        return self.bounding_box

    @caching.cache_decorator
    def area(self):
        """
        Surface area of the current sphere primitive.

        Returns
        --------
        area: float, surface area of the sphere Primitive
        """

        area = 4.0 * np.pi * (self.primitive.radius ** 2)
        return area

    @caching.cache_decorator
    def volume(self):
        """
        Volume of the current sphere primitive.

        Returns
        --------
        volume: float, volume of the sphere Primitive
        """

        volume = (4.0 * np.pi * (self.primitive.radius ** 3)) / 3.0
        return volume

    @caching.cache_decorator
    def moment_inertia(self):
        """
        The analytic inertia tensor of the sphere primitive.

        Returns
        ----------
        tensor: (3, 3) float
          3D inertia tensor.
        """
        return inertia.sphere_inertia(
            mass=self.volume,
            radius=self.primitive.radius)

    def _create_mesh(self):
        log.debug('creating mesh for Sphere primitive')
        unit = creation.icosphere(
            subdivisions=self.primitive.subdivisions,
            radius=self.primitive.radius)

        # apply the center offset here
        self._cache['vertices'] = unit.vertices + self.primitive.center
        self._cache['faces'] = unit.faces
        self._cache['face_normals'] = unit.face_normals


class Box(_Primitive):
    def __init__(self,
                 extents=None,
                 transform=None,
                 bounds=None,
                 mutable=True):
        """
        Create a Box Primitive as a subclass of Trimesh

        Parameters
        ----------
        extents : Optional[ndarray] (3,) float
          Length of each side of the 3D box.
        transform : Optional[ndarray] (4, 4) float
          Homogeneous transformation matrix for box center.
        bounds : Optional[ndarray] (2, 3) float
          Axis aligned bounding box, if passed extents and
          transform will be derived from this.
        mutable : bool
          Are extents and transform mutable after creation.
        """
        super(Box, self).__init__()
        defaults = {'transform': np.eye(4),
                    'extents': np.ones(3)}

        if bounds is not None:
            # validate the multiple forms of input available here
            if extents is not None or transform is not None:
                raise ValueError(
                    'if `bounds` is passed `extents` and `transform` must not be!')
            bounds = np.array(bounds, dtype=np.float64)
            if bounds.shape != (2, 3):
                raise ValueError('`bounds` must be (2, 3) float')
            # create extents from AABB
            extents = bounds.ptp(axis=0)
            # translate to the center of the box
            transform = np.eye(4)
            transform[:3, 3] = bounds[0] + extents / 2.0

        self.primitive = _PrimitiveAttributes(
            self,
            defaults=defaults,
            kwargs={'extents': extents,
                    'transform': transform},
            mutable=mutable)

    def to_dict(self):
        """
        Get a copy of the current Box primitive as
        a JSON-serializable dict that matches the schema
        in `trimesh/resources/schema/box.schema.json`

        Returns
        ----------
        as_dict : dict
          Serializable data for this primitive.
        """
        return {'kind': 'box',
                'transform': self.primitive.transform.tolist(),
                'extents': self.primitive.extents.tolist()}

    @property
    def transform(self):
        return self.primitive.transform

    def sample_volume(self, count):
        """
        Return random samples from inside the volume of the box.

        Parameters
        -------------
        count : int
          Number of samples to return

        Returns
        ----------
        samples : (count, 3) float
          Points inside the volume
        """
        samples = sample.volume_rectangular(
            extents=self.primitive.extents,
            count=count,
            transform=self.primitive.transform)
        return samples

    def sample_grid(self, count=None, step=None):
        """
        Return a 3D grid which is contained by the box.
        Samples are either 'step' distance apart, or there are
        'count' samples per box side.

        Parameters
        -----------
        count : int or (3,) int
          If specified samples are spaced with np.linspace
        step : float or (3,) float
          If specified samples are spaced with np.arange

        Returns
        -----------
        grid : (n, 3) float
          Points inside the box
        """

        if (count is not None and
                step is not None):
            raise ValueError('only step OR count can be specified!')

        # create pre- transform bounds from extents
        bounds = np.array([-self.primitive.extents,
                           self.primitive.extents]) * .5

        if step is not None:
            grid = util.grid_arange(bounds, step=step)
        elif count is not None:
            grid = util.grid_linspace(bounds, count=count)
        else:
            raise ValueError('either count or step must be specified!')

        transformed = tf.transform_points(
            grid, matrix=self.primitive.transform)
        return transformed

    @property
    def is_oriented(self):
        """
        Returns whether or not the current box is rotated at all.
        """
        if util.is_shape(self.primitive.transform, (4, 4)):
            return not np.allclose(self.primitive.transform[
                                   0:3, 0:3], np.eye(3))
        else:
            return False

    @caching.cache_decorator
    def volume(self):
        """
        Volume of the box Primitive.

        Returns
        --------
        volume : float
          Volume of box.
        """
        volume = float(np.prod(self.primitive.extents))
        return volume

    def _create_mesh(self):
        log.debug('creating mesh for Box primitive')
        box = creation.box(extents=self.primitive.extents,
                           transform=self.primitive.transform)

        self._cache.cache.update(box._cache.cache)
        self._cache['vertices'] = box.vertices
        self._cache['faces'] = box.faces
        self._cache['face_normals'] = box.face_normals

    def as_outline(self):
        """
        Return a Path3D containing the outline of the box.

        Returns
        -----------
        outline : trimesh.path.Path3D
          Outline of box primitive
        """
        # do the import in function to keep soft dependency
        from .path.creation import box_outline
        # return outline with same size as primitive
        return box_outline(
            extents=self.primitive.extents,
            transform=self.primitive.transform)


class Extrusion(_Primitive):
    def __init__(self,
                 polygon=None,
                 transform=None,
                 height=1.0,
                 mutable=True):
        """
        Create an Extrusion primitive, which
        is a subclass of Trimesh.

        Parameters
        ----------
        polygon : shapely.geometry.Polygon
          Polygon to extrude
        transform : (4, 4) float
          Transform to apply after extrusion
        height : float
          Height to extrude polygon by
        mutable : bool
          Are extents and transform mutable after creation.
        """
        # do the import here, fail early if Shapely isn't installed
        from shapely.geometry import Point

        # run the Trimesh init
        super(Extrusion, self).__init__()
        # set default values
        defaults = {'polygon': Point([0, 0]).buffer(1.0),
                    'transform': np.eye(4),
                    'height': 1.0}

        self.primitive = _PrimitiveAttributes(
            self,
            defaults=defaults,
            kwargs={'transform': transform,
                    'polygon': polygon,
                    'height': height},
            mutable=mutable)

    @caching.cache_decorator
    def area(self):
        """
        The surface area of the primitive extrusion.

        Calculated from polygon and height to avoid mesh creation.

        Returns
        ----------
        area: float
          Surface area of 3D extrusion
        """
        # area of the sides of the extrusion
        area = abs(self.primitive.height *
                   self.primitive.polygon.length)
        # area of the two caps of the extrusion
        area += self.primitive.polygon.area * 2
        return area

    @caching.cache_decorator
    def volume(self):
        """
        The volume of the Extrusion primitive.
        Calculated from polygon and height to avoid mesh creation.

        Returns
        ----------
        volume : float
          Volume of 3D extrusion
        """
        # height may be negative
        volume = abs(self.primitive.polygon.area *
                     self.primitive.height)
        return volume

    @caching.cache_decorator
    def direction(self):
        """
        Based on the extrudes transform what is the
        vector along which the polygon will be extruded.

        Returns
        ---------
        direction : (3,) float
          Unit direction vector
        """
        # only consider rotation and signed height
        direction = np.dot(
            self.primitive.transform[:3, :3],
            [0.0, 0.0, np.sign(self.primitive.height)])
        return direction

    @property
    def origin(self):
        """
        Based on the extrude transform what is the
        origin of the plane it is extruded from.

        Returns
        -----------
        origin : (3,) float
          Origin of extrusion plane
        """
        return self.primitive.transform[:3, 3]

    @property
    def transform(self):
        return self.primitive.transform

    @caching.cache_decorator
    def bounding_box_oriented(self):
        # no docstring for inheritance
        # calculate OBB using 2D polygon and known axis
        from . import bounds
        # find the 2D bounding box using the polygon
        to_origin, box = bounds.oriented_bounds_2D(
            self.primitive.polygon.exterior.coords)
        #  3D extents
        extents = np.append(box, abs(self.primitive.height))
        # calculate to_3D transform from 2D obb
        rotation_Z = np.linalg.inv(tf.planar_matrix_to_3D(to_origin))
        rotation_Z[2, 3] = self.primitive.height / 2.0
        # combine the 2D OBB transformation with the 2D projection transform
        to_3D = np.dot(self.primitive.transform, rotation_Z)
        obb = Box(transform=to_3D,
                  extents=extents,
                  mutable=False)
        return obb

    def slide(self, distance):
        """
        Alter the transform of the current extrusion to slide it
        along its extrude_direction vector

        Parameters
        -----------
        distance : float
          Distance along self.extrude_direction to move
        """
        distance = float(distance)
        translation = np.eye(4)
        translation[2, 3] = distance
        new_transform = np.dot(self.primitive.transform.copy(),
                               translation.copy())
        self.primitive.transform = new_transform

    def buffer(self, distance, distance_height=None, **kwargs):
        """
        Return a new Extrusion object which is expanded in profile
        and in height by a specified distance.

        Parameters
        --------------
        distance : float
          Distance to buffer polygon
        distance_height : float
          Distance to buffer above and below extrusion
        kwargs : dict
          Passed to Extrusion constructor

        Returns
        ----------
        buffered : primitives.Extrusion
          Extrusion object with new values
        """
        distance = float(distance)
        # if not specified use same distance for everything
        if distance_height is None:
            distance_height = distance

        # start with current height
        height = self.primitive.height
        # if current height is negative offset by negative amount
        height += np.sign(height) * 2.0 * distance_height

        # create a new extrusion with a buffered polygon
        # use type(self) vs Extrusion to handle subclasses
        buffered = type(self)(
            transform=self.primitive.transform.copy(),
            polygon=self.primitive.polygon.buffer(distance),
            height=height,
            **kwargs)

        # slide the stock along the axis
        buffered.slide(-np.sign(height) * distance_height)

        return buffered

    def to_dict(self):
        """
        Get a copy of the current Extrusion primitive as
        a JSON-serializable dict that matches the schema
        in `trimesh/resources/schema/extrusion.schema.json`

        Returns
        ----------
        as_dict : dict
          Serializable data for this primitive.
        """
        return {'kind': 'extrusion',
                'polygon': self.primitive.polygon.wkt,
                'transform': self.primitive.transform.tolist(),
                'height': float(self.primitive.height)}

    def _create_mesh(self):
        log.debug('creating mesh for Extrusion primitive')
        # extrude the polygon along Z
        mesh = creation.extrude_polygon(
            polygon=self.primitive.polygon,
            height=self.primitive.height,
            transform=self.primitive.transform)

        # check volume here in unit tests
        if tol.strict and mesh.volume < 0.0:
            raise ValueError('matrix inverted mesh!')

        # cache mesh geometry in the primitive
        self._cache['vertices'] = mesh.vertices
        self._cache['faces'] = mesh.faces


# trimesh/proximity.py

"""
proximity.py
---------------

Query mesh- point proximity.
"""
import numpy as np

from . import util

from .grouping import group_min
from .constants import tol, log_time
from .triangles import closest_point as _corresponding
from .triangles import points_to_barycentric

try:
    from scipy.spatial import cKDTree
except BaseException as E:
    from .exceptions import ExceptionWrapper
    cKDTree = ExceptionWrapper(E)


def nearby_faces(mesh, points):
    """
    For each point find nearby faces relatively quickly.

    The closest point on the mesh to the queried point is guaranteed to be
    on one of the faces listed.

    Does this by finding the nearest vertex on the mesh to each point, and
    then returns all the faces that intersect the axis aligned bounding box
    centered at the queried point and extending to the nearest vertex.

    Parameters
    ----------
    mesh : trimesh.Trimesh
      Mesh to query.
    points : (n, 3) float
       Points in space

    Returns
    -----------
    candidates : (points,) int
      Sequence of indexes for mesh.faces
    """
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    # an r-tree containing the axis aligned bounding box for every triangle
    rtree = mesh.triangles_tree
    # a kd-tree containing every vertex of the mesh
    kdtree = cKDTree(mesh.vertices[mesh.referenced_vertices])

    # query the distance to the nearest vertex to get AABB of a sphere
    distance_vertex = kdtree.query(points)[0].reshape((-1, 1))
    distance_vertex += tol.merge

    # axis aligned bounds
    bounds = np.column_stack((points - distance_vertex,
                              points + distance_vertex))

    # faces that intersect axis aligned bounding box
    candidates = [list(rtree.intersection(b)) for b in bounds]

    return candidates


def closest_point_naive(mesh, points):
    """
    Given a mesh and a list of points find the closest point
    on any triangle.

    Does this by constructing a very large intermediate array and
    comparing every point to every triangle.

    Parameters
    ----------
    mesh : Trimesh
      Takes mesh to have same interfaces as `closest_point`
    points : (m, 3) float
      Points in space

    Returns
    ----------
    closest : (m, 3) float
      Closest point on triangles for each point
    distance : (m,) float
      Distances between point and triangle
    triangle_id : (m,) int
      Index of triangle containing closest point
    """
    # get triangles from mesh
    triangles = mesh.triangles.view(np.ndarray)
    # establish that input points are sane
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('triangles shape incorrect')
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)')

    # create a giant tiled array of each point tiled len(triangles) times
    points_tiled = np.tile(points, (1, len(triangles)))
    on_triangle = np.array([_corresponding(
        triangles, i.reshape((-1, 3))) for i in points_tiled])

    # distance squared
    distance_2 = [((i - q)**2).sum(axis=1)
                  for i, q in zip(on_triangle, points)]

    triangle_id = np.array([i.argmin() for i in distance_2])

    # closest cartesian point
    closest = np.array([g[i] for i, g in zip(triangle_id, on_triangle)])
    distance = np.array([g[i] for i, g in zip(triangle_id, distance_2)]) ** .5

    return closest, distance, triangle_id


def closest_point(mesh, points):
    """
    Given a mesh and a list of points find the closest point
    on any triangle.

    Parameters
    ----------
    mesh : trimesh.Trimesh
      Mesh to query
    points : (m, 3) float
      Points in space

    Returns
    ----------
    closest : (m, 3) float
      Closest point on triangles for each point
    distance : (m,)  float
      Distance to mesh.
    triangle_id : (m,) int
      Index of triangle containing closest point
    """
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    # do a tree- based query for faces near each point
    candidates = nearby_faces(mesh, points)
    # view triangles as an ndarray so we don't have to recompute
    # the MD5 during all of the subsequent advanced indexing
    triangles = mesh.triangles.view(np.ndarray)

    # create the corresponding list of triangles
    # and query points to send to the closest_point function
    all_candidates = np.concatenate(candidates)

    num_candidates = list(map(len, candidates))
    tile_idxs = np.repeat(np.arange(len(points)), num_candidates)
    query_point = points[tile_idxs, :]

    query_tri = triangles[all_candidates]

    # do the computation for closest point
    query_close = _corresponding(query_tri, query_point)
    query_group = np.cumsum(num_candidates)[:-1]

    # vectors and distances for
    # closest point to query point
    query_vector = query_point - query_close
    query_distance = util.diagonal_dot(query_vector, query_vector)

    # get best two candidate indices by arg-sorting the per-query_distances
    qds = np.array_split(query_distance, query_group)
    idxs = np.int32([qd.argsort()[:2] if len(qd) > 1 else [0, 0]
                    for qd in qds])
    idxs[1:] += query_group.reshape(-1, 1)

    # points, distances and triangle ids for best two candidates
    two_points = query_close[idxs]
    two_dists = query_distance[idxs]
    two_candidates = all_candidates[idxs]

    # the first candidate is the best result for unambiguous cases
    result_close = query_close[idxs[:, 0]]
    result_tid = two_candidates[:, 0]
    result_distance = two_dists[:, 0]

    # however: same closest point on two different faces
    # find the best one and correct triangle ids if necessary
    check_distance = two_dists.ptp(axis=1) < tol.merge
    check_magnitude = np.all(np.abs(two_dists) > tol.merge, axis=1)

    # mask results where corrections may be apply
    c_mask = np.bitwise_and(check_distance, check_magnitude)

    # get two face normals for the candidate points
    normals = mesh.face_normals[two_candidates[c_mask]]
    # compute normalized surface-point to query-point vectors
    vectors = (query_vector[idxs[c_mask]] /
               two_dists[c_mask].reshape(-1, 2, 1) ** 0.5)
    # compare enclosed angle for both face normals
    dots = (normals * vectors).sum(axis=2)

    # take the idx with the most positive angle
    # allows for selecting the correct candidate triangle id
    c_idxs = dots.argmax(axis=1)

    # correct triangle ids where necessary
    # closest point and distance remain valid
    result_tid[c_mask] = two_candidates[c_mask, c_idxs]
    result_distance[c_mask] = two_dists[c_mask, c_idxs]
    result_close[c_mask] = two_points[c_mask, c_idxs]

    # we were comparing the distance squared so
    # now take the square root in one vectorized operation
    result_distance **= .5

    return result_close, result_distance, result_tid


def signed_distance(mesh, points):
    """
    Find the signed distance from a mesh to a list of points.

    * Points OUTSIDE the mesh will have NEGATIVE distance
    * Points within tol.merge of the surface will have POSITIVE distance
    * Points INSIDE the mesh will have POSITIVE distance

    Parameters
    -----------
    mesh : trimesh.Trimesh
      Mesh to query.
    points : (n, 3) float
      Points in space

    Returns
    ----------
    signed_distance : (n,) float
      Signed distance from point to mesh
    """
    # make sure we have a numpy array
    points = np.asanyarray(points, dtype=np.float64)

    # find the closest point on the mesh to the queried points
    closest, distance, triangle_id = closest_point(mesh, points)

    # we only care about nonzero distances
    nonzero = distance > tol.merge

    if not nonzero.any():
        return distance

    # For closest points that project directly in to the triangle, compute sign from
    # triangle normal Project each point in to the closest triangle plane
    nonzero = np.where(nonzero)[0]
    normals = mesh.face_normals[triangle_id]
    projection = (points[nonzero] -
                  (normals[nonzero].T * np.einsum(
                      "ij,ij->i",
                      points[nonzero] - closest[nonzero],
                      normals[nonzero])).T)

    # Determine if the projection lies within the closest triangle
    barycentric = points_to_barycentric(
        mesh.triangles[triangle_id[nonzero]],
        projection)
    ontriangle = ~((
        (barycentric < -tol.merge) | (barycentric > 1 + tol.merge)
    ).any(axis=1))

    # Where projection does lie in the triangle, compare vector to projection to the
    # triangle normal to compute sign
    sign = np.sign(np.einsum(
        "ij,ij->i",
        normals[nonzero[ontriangle]],
        points[nonzero[ontriangle]] - projection[ontriangle]))
    distance[nonzero[ontriangle]] *= -1.0 * sign

    # For all other triangles, resort to raycasting against the entire mesh
    inside = mesh.ray.contains_points(points[nonzero[~ontriangle]])
    sign = (inside.astype(int) * 2) - 1.0

    # apply sign to previously computed distance
    distance[nonzero[~ontriangle]] *= sign

    return distance


class NearestQueryResult:
    """
    Stores the nearest points and attributes for nearest points queries.
    """

    def __init__(self):
        self.nearest = None
        self.distances = None
        self.normals = None
        self.triangle_indices = None
        self.barycentric_coordinates = None
        self.interpolated_normals = None
        self.vertex_indices = None

    def has_normals(self):
        return self.normals is not None or self.interpolated_normals is not None


class ProximityQuery(object):
    """
    Proximity queries for the current mesh.
    """

    def __init__(self, mesh):
        self._mesh = mesh

    @log_time
    def on_surface(self, points):
        """
        Given list of points, for each point find the closest point
        on any triangle of the mesh.

        Parameters
        ----------
        points : (m,3) float, points in space

        Returns
        ----------
        closest : (m, 3) float
          Closest point on triangles for each point
        distance : (m,) float
          Distance to surface
        triangle_id : (m,) int
          Index of closest triangle for each point.
        """
        return closest_point(mesh=self._mesh,
                             points=points)

    def vertex(self, points):
        """
        Given a set of points, return the closest vertex index to each point

        Parameters
        ----------
        points : (n, 3) float
          Points in space

        Returns
        ----------
        distance : (n,) float
           Distance from source point to vertex.
        vertex_id : (n,) int
          Index of mesh.vertices for closest vertex.
        """
        tree = self._mesh.kdtree
        return tree.query(points)

    def signed_distance(self, points):
        """
        Find the signed distance from a mesh to a list of points.

        * Points OUTSIDE the mesh will have NEGATIVE distance
        * Points within tol.merge of the surface will have POSITIVE distance
        * Points INSIDE the mesh will have POSITIVE distance

        Parameters
        -----------
        points : (n, 3) float
          Points in space

        Returns
        ----------
        signed_distance : (n,) float
          Signed distance from point to mesh.
        """
        return signed_distance(self._mesh, points)


def longest_ray(mesh, points, directions):
    """
    Find the lengths of the longest rays which do not intersect the mesh
    cast from a list of points in the provided directions.

    Parameters
    -----------
    points : (n, 3) float
      Points in space.
    directions : (n, 3) float
      Directions of rays.

    Returns
    ----------
    signed_distance : (n,) float
      Length of rays.
    """
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    directions = np.asanyarray(directions, dtype=np.float64)
    if not util.is_shape(directions, (-1, 3)):
        raise ValueError('directions must be (n,3)!')

    if len(points) != len(directions):
        raise ValueError('number of points must equal number of directions!')

    faces, rays, locations = mesh.ray.intersects_id(points, directions,
                                                    return_locations=True,
                                                    multiple_hits=True)
    if len(rays) > 0:
        distances = np.linalg.norm(locations - points[rays],
                                   axis=1)
    else:
        distances = np.array([])

    # Reject intersections at distance less than tol.planar
    rays = rays[distances > tol.planar]
    distances = distances[distances > tol.planar]

    # Add infinite length for those with no valid intersection
    no_intersections = np.setdiff1d(np.arange(len(points)), rays)
    rays = np.concatenate((rays, no_intersections))
    distances = np.concatenate((distances,
                                np.repeat(np.inf,
                                          len(no_intersections))))
    return group_min(rays, distances)


def max_tangent_sphere(mesh,
                       points,
                       inwards=True,
                       normals=None,
                       threshold=1e-6,
                       max_iter=100):
    """
    Find the center and radius of the sphere which is tangent to
    the mesh at the given point and at least one more point with no
    non-tangential intersections with the mesh.

    Masatomo Inui, Nobuyuki Umezu & Ryohei Shimane (2016)
    Shrinking sphere:
    A parallel algorithm for computing the thickness of 3D objects,
    Computer-Aided Design and Applications, 13:2, 199-207,
    DOI: 10.1080/16864360.2015.1084186

    Parameters
    ----------
    points : (n, 3) float
      Points in space.
    inwards : bool
      Whether to have the sphere inside or outside the mesh.
    normals : (n, 3) float or None
      Normals of the mesh at the given points
      if is None computed automatically.

    Returns
    ----------
    centers : (n,3) float
      Centers of spheres
    radii : (n,) float
      Radii of spheres
    """
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    if normals is not None:
        normals = np.asanyarray(normals, dtype=np.float64)
        if not util.is_shape(normals, (-1, 3)):
            raise ValueError('normals must be (n,3)!')

        if len(points) != len(normals):
            raise ValueError('number of points must equal number of normals!')
    else:
        normals = mesh.face_normals[closest_point(mesh, points)[2]]

    if inwards:
        normals = -normals

    # Find initial tangent spheres
    distances = longest_ray(mesh, points, normals)
    radii = distances * 0.5
    not_converged = np.ones(len(points), dtype=bool)  # boolean mask

    # If ray is infinite, find the vertex which is furthest from our point
    # when projected onto the ray. I.e. find v which maximises
    # (v-p).n = v.n - p.n.
    # We use a loop rather a vectorised approach to reduce memory cost
    # it also seems to run faster.
    for i in np.where(np.isinf(distances))[0]:
        projections = np.dot(mesh.vertices - points[i], normals[i])

        # If no points lie outside the tangent plane, then the radius is infinite
        # otherwise we have a point outside the tangent plane, take the one with maximal
        # projection
        if projections.max() < tol.planar:
            radii[i] = np.inf
            not_converged[i] = False
        else:
            vertex = mesh.vertices[projections.argmax()]
            radii[i] = (np.dot(vertex - points[i], vertex - points[i]) /
                        (2 * np.dot(vertex - points[i], normals[i])))

    # Compute centers
    centers = points + normals * np.nan_to_num(radii.reshape(-1, 1))
    centers[np.isinf(radii)] = [np.nan, np.nan, np.nan]

    # Our iterative process terminates when the difference in sphere
    # radius is less than threshold*D
    D = np.linalg.norm(mesh.bounds[1] - mesh.bounds[0])
    convergence_threshold = threshold * D
    n_iter = 0
    while not_converged.sum() > 0 and n_iter < max_iter:
        n_iter += 1
        n_points, n_dists, n_faces = mesh.nearest.on_surface(
            centers[not_converged])

        # If the distance to the nearest point is the same as the distance
        # to the start point then we are done.
        done = np.abs(
            n_dists -
            np.linalg.norm(
                centers[not_converged] -
                points[not_converged],
                axis=1)) < tol.planar
        not_converged[np.where(not_converged)[0][done]] = False

        # Otherwise find the radius and center of the sphere tangent to the mesh
        # at the point and the nearest point.
        diff = n_points[~done] - points[not_converged]
        old_radii = radii[not_converged].copy()
        # np.einsum produces element wise dot product
        radii[not_converged] = (np.einsum('ij, ij->i',
                                          diff,
                                          diff) /
                                (2 * np.einsum('ij, ij->i',
                                               diff,
                                               normals[not_converged])))
        centers[not_converged] = points[not_converged] + \
            normals[not_converged] * radii[not_converged].reshape(-1, 1)

        # If change in radius is less than threshold we have converged
        cvged = old_radii - radii[not_converged] < convergence_threshold
        not_converged[np.where(not_converged)[0][cvged]] = False

    return centers, radii


def thickness(mesh,
              points,
              exterior=False,
              normals=None,
              method='max_sphere'):
    """
    Find the thickness of the mesh at the given points.

    Parameters
    ----------
    points : (n, 3) float
      Points in space
    exterior : bool
      Whether to compute the exterior thickness
      (a.k.a. reach)
    normals : (n, 3) float
      Normals of the mesh at the given points
      If is None computed automatically.
    method : string
      One of 'max_sphere' or 'ray'

    Returns
    ----------
    thickness : (n,) float
      Thickness at given points.
    """
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(points, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    if normals is not None:
        normals = np.asanyarray(normals, dtype=np.float64)
        if not util.is_shape(normals, (-1, 3)):
            raise ValueError('normals must be (n,3)!')

        if len(points) != len(normals):
            raise ValueError('number of points must equal number of normals!')
    else:
        normals = mesh.face_normals[closest_point(mesh, points)[2]]

    if method == 'max_sphere':
        centers, radius = max_tangent_sphere(mesh=mesh,
                                             points=points,
                                             inwards=not exterior,
                                             normals=normals)
        thickness = radius * 2
        return thickness

    elif method == 'ray':
        if exterior:
            return longest_ray(mesh, points, normals)
        else:
            return longest_ray(mesh, points, -normals)
    else:
        raise ValueError('Invalid method, use "max_sphere" or "ray"')


# trimesh/registration.py

"""
registration.py
---------------

Functions for registering (aligning) point clouds with meshes.
"""

import numpy as np


from . import util
from . import bounds
from . import transformations

from .points import PointCloud, plane_fit
from .geometry import weighted_vertex_normals
from .triangles import normals, angles, cross
from .transformations import transform_points

try:
    from scipy.spatial import cKDTree
    import scipy.sparse as sparse
except BaseException as E:
    # wrapping just ImportError fails in some cases
    # will raise the error when someone tries to use KDtree
    from . import exceptions
    cKDTree = exceptions.ExceptionWrapper(E)
    sparse = exceptions.ExceptionWrapper(E)


def mesh_other(mesh,
               other,
               samples=500,
               scale=False,
               icp_first=10,
               icp_final=50,
               **kwargs):
    """
    Align a mesh with another mesh or a PointCloud using
    the principal axes of inertia as a starting point which
    is refined by iterative closest point.

    Parameters
    ------------
    mesh : trimesh.Trimesh object
      Mesh to align with other
    other : trimesh.Trimesh or (n, 3) float
      Mesh or points in space
    samples : int
      Number of samples from mesh surface to align
    scale : bool
      Allow scaling in transform
    icp_first : int
      How many ICP iterations for the 9 possible
      combinations of sign flippage
    icp_final : int
      How many ICP iterations for the closest
      candidate from the wider search
    kwargs : dict
      Passed through to `icp`, which passes through to `procrustes`

    Returns
    -----------
    mesh_to_other : (4, 4) float
      Transform to align mesh to the other object
    cost : float
      Average squared distance per point
    """

    def key_points(m, count):
        """
        Return a combination of mesh vertices and surface samples
        with vertices chosen by likelihood to be important
        to registration.
        """
        if len(m.vertices) < (count / 2):
            return np.vstack((
                m.vertices,
                m.sample(count - len(m.vertices))))
        else:
            return m.sample(count)

    if not util.is_instance_named(mesh, 'Trimesh'):
        raise ValueError('mesh must be Trimesh object!')

    inverse = True
    search = mesh
    # if both are meshes use the smaller one for searching
    if util.is_instance_named(other, 'Trimesh'):
        if len(mesh.vertices) > len(other.vertices):
            # do the expensive tree construction on the
            # smaller mesh and query the others points
            search = other
            inverse = False
            points = key_points(m=mesh, count=samples)
            points_mesh = mesh
        else:
            points_mesh = other
            points = key_points(m=other, count=samples)

        if points_mesh.is_volume:
            points_PIT = points_mesh.principal_inertia_transform
        else:
            points_PIT = points_mesh.bounding_box_oriented.principal_inertia_transform

    elif util.is_shape(other, (-1, 3)):
        # case where other is just points
        points = other
        points_PIT = bounds.oriented_bounds(points)[0]
    else:
        raise ValueError('other must be mesh or (n, 3) points!')

    # get the transform that aligns the search mesh principal
    # axes of inertia with the XYZ axis at the origin
    if search.is_volume:
        search_PIT = search.principal_inertia_transform
    else:
        search_PIT = search.bounding_box_oriented.principal_inertia_transform

    # transform that moves the principal axes of inertia
    # of the search mesh to be aligned with the best- guess
    # principal axes of the points
    search_to_points = np.dot(np.linalg.inv(points_PIT),
                              search_PIT)

    # permutations of cube rotations
    # the principal inertia transform has arbitrary sign
    # along the 3 major axis so try all combinations of
    # 180 degree rotations with a quick first ICP pass
    cubes = np.array([np.eye(4) * np.append(diag, 1)
                      for diag in [[1, 1, 1],
                                   [1, 1, -1],
                                   [1, -1, 1],
                                   [-1, 1, 1],
                                   [-1, -1, 1],
                                   [-1, 1, -1],
                                   [1, -1, -1],
                                   [-1, -1, -1]]])

    # loop through permutations and run iterative closest point
    costs = np.ones(len(cubes)) * np.inf
    transforms = [None] * len(cubes)
    centroid = search.centroid

    for i, flip in enumerate(cubes):
        # transform from points to search mesh
        # flipped around the centroid of search
        a_to_b = np.dot(
            transformations.transform_around(flip, centroid),
            np.linalg.inv(search_to_points))

        # run first pass ICP
        matrix, junk, cost = icp(a=points,
                                 b=search,
                                 initial=a_to_b,
                                 max_iterations=int(icp_first),
                                 scale=scale)

        # save transform and costs from ICP
        transforms[i] = matrix
        costs[i] = cost

    # run a final ICP refinement step
    matrix, junk, cost = icp(a=points,
                             b=search,
                             initial=transforms[np.argmin(costs)],
                             max_iterations=int(icp_final),
                             scale=scale)

    # convert to per- point distance average
    cost /= len(points)

    # we picked the smaller mesh to construct the tree
    # on so we may have calculated a transform backwards
    # to save computation, so just invert matrix here
    if inverse:
        mesh_to_other = np.linalg.inv(matrix)
    else:
        mesh_to_other = matrix

    return mesh_to_other, cost


def procrustes(a,
               b,
               weights=None,
               reflection=True,
               translation=True,
               scale=True,
               return_cost=True):
    """
    Perform Procrustes' analysis subject to constraints. Finds the
    transformation T mapping a to b which minimizes the square sum
    distances between Ta and b, also called the cost. Optionally
    specify different weights for the points in a to minimize the
    weighted square sum distances between Ta and b. This can
    improve transformation robustness on noisy data if the points'
    probability distribution is known.

    Parameters
    ----------
    a : (n,3) float
      List of points in space
    b : (n,3) float
      List of points in space
    weights : (n,) float
      List of floats representing how much weight is assigned to each point of a
    reflection : bool
      If the transformation is allowed reflections
    translation : bool
      If the transformation is allowed translations
    scale : bool
      If the transformation is allowed scaling
    return_cost : bool
      Whether to return the cost and transformed a as well

    Returns
    ----------
    matrix : (4,4) float
      The transformation matrix sending a to b
    transformed : (n,3) float
      The image of a under the transformation
    cost : float
      The cost of the transformation
    """

    a = np.asanyarray(a, dtype=np.float64)
    b = np.asanyarray(b, dtype=np.float64)
    if not util.is_shape(a, (-1, 3)) or not util.is_shape(b, (-1, 3)):
        raise ValueError('points must be (n,3)!')
    if len(a) != len(b):
        raise ValueError('a and b must contain same number of points!')
    if weights is not None:
        w = np.asanyarray(weights, dtype=np.float64)
        if len(w) != len(a):
            raise ValueError("weights must have same length as a and b!")
        w_norm = (w / w.sum()).reshape((-1, 1))

    # Remove translation component
    if translation:
        # acenter is a weighted average of the individual points.
        if weights is None:
            acenter = a.mean(axis=0)
        else:
            acenter = (a * w_norm).sum(axis=0)
        bcenter = b.mean(axis=0)
    else:
        acenter = np.zeros(a.shape[1])
        bcenter = np.zeros(b.shape[1])

    # Remove scale component
    if scale:
        if weights is None:
            ascale = np.sqrt(((a - acenter)**2).sum() / len(a))
            # ascale is the square root of weighted average of the
            # squared difference
            # between each point and acenter.
        else:
            ascale = np.sqrt((((a - acenter)**2) * w_norm).sum())

        bscale = np.sqrt(((b - bcenter)**2).sum() / len(b))
    else:
        ascale = 1
        bscale = 1

    # Use SVD to find optimal orthogonal matrix R
    # constrained to det(R) = 1 if necessary.
    # w_mat is multiplied with the centered and scaled a, such that the points
    # can be weighted differently.

    if weights is None:
        target = np.dot(((b - bcenter) / bscale).T,
                        ((a - acenter) / ascale))
    else:
        target = np.dot(
            ((b - bcenter) / bscale).T,
            ((a - acenter) / ascale) * w.reshape((-1, 1)))

    u, s, vh = np.linalg.svd(target)

    if reflection:
        R = np.dot(u, vh)
    else:
        # no reflection allowed, so determinant must be 1.0
        R = np.dot(np.dot(u, np.diag(
            [1, 1, np.linalg.det(np.dot(u, vh))])), vh)

    # Compute our 4D transformation matrix encoding
    # a -> (R @ (a - acenter)/ascale) * bscale + bcenter
    #    = (bscale/ascale)R @ a + (bcenter - (bscale/ascale)R @ acenter)
    translation = bcenter - (bscale / ascale) * np.dot(R, acenter)
    matrix = np.hstack((bscale / ascale * R, translation.reshape(-1, 1)))
    matrix = np.vstack(
        (matrix, np.array([0.] * (a.shape[1]) + [1.]).reshape(1, -1)))

    if return_cost:
        transformed = transform_points(a, matrix)
        cost = ((b - transformed)**2).mean()
        return matrix, transformed, cost
    else:
        return matrix


def icp(a,
        b,
        initial=None,
        threshold=1e-5,
        max_iterations=20,
        **kwargs):
    """
    Apply the iterative closest point algorithm to align a point cloud with
    another point cloud or mesh. Will only produce reasonable results if the
    initial transformation is roughly correct. Initial transformation can be
    found by applying Procrustes' analysis to a suitable set of landmark
    points (often picked manually).

    Parameters
    ----------
    a : (n,3) float
      List of points in space.
    b : (m,3) float or Trimesh
      List of points in space or mesh.
    initial : (4,4) float
      Initial transformation.
    threshold : float
      Stop when change in cost is less than threshold
    max_iterations : int
      Maximum number of iterations
    kwargs : dict
      Args to pass to procrustes

    Returns
    ----------
    matrix : (4,4) float
      The transformation matrix sending a to b
    transformed : (n,3) float
      The image of a under the transformation
    cost : float
      The cost of the transformation
    """

    a = np.asanyarray(a, dtype=np.float64)
    if not util.is_shape(a, (-1, 3)):
        raise ValueError('points must be (n,3)!')

    if initial is None:
        initial = np.eye(4)

    is_mesh = util.is_instance_named(b, 'Trimesh')
    if not is_mesh:
        b = np.asanyarray(b, dtype=np.float64)
        if not util.is_shape(b, (-1, 3)):
            raise ValueError('points must be (n,3)!')
        btree = cKDTree(b)

    # transform a under initial_transformation
    a = transform_points(a, initial)
    total_matrix = initial

    # start with infinite cost
    old_cost = np.inf

    # avoid looping forever by capping iterations
    for _ in range(max_iterations):
        # Closest point in b to each point in a
        if is_mesh:
            closest, distance, faces = b.nearest.on_surface(a)
        else:
            distances, ix = btree.query(a, 1)
            closest = b[ix]

        # align a with closest points
        matrix, transformed, cost = procrustes(a=a,
                                               b=closest,
                                               **kwargs)

        # update a with our new transformed points
        a = transformed
        total_matrix = np.dot(matrix, total_matrix)

        if old_cost - cost < threshold:
            break
        else:
            old_cost = cost

    return total_matrix, transformed, cost


def _normalize_by_source(source_mesh, target_geometry, target_positions):
    # Utility function to put the source mesh in [-1, 1]^3 and transform
    # target geometry accordingly
    if not util.is_instance_named(target_geometry, 'Trimesh') and \
            not isinstance(target_geometry, PointCloud):
        vertices = np.asanyarray(target_geometry)
        target_geometry = PointCloud(vertices)
    centroid, scale = source_mesh.centroid, source_mesh.scale
    source_mesh.vertices = (source_mesh.vertices - centroid[None, :]) / scale
    # Dont forget to also transform the target positions
    target_geometry.vertices = (
        target_geometry.vertices - centroid[None, :]) / scale
    if target_positions is not None:
        target_positions = (target_positions - centroid[None, :]) / scale
    return target_geometry, target_positions, centroid, scale


def _denormalize_by_source(
        source_mesh,
        target_geometry,
        target_positions,
        result,
        centroid,
        scale):
    # Utility function to transform source mesh from
    # [-1, 1]^3 to its original transform
    # and transform target geometry accordingly
    source_mesh.vertices = scale * source_mesh.vertices + centroid[None, :]
    target_geometry.vertices = scale * \
        target_geometry.vertices + centroid[None, :]
    if target_positions is not None:
        target_positions = scale * target_positions + centroid[None, :]
    if isinstance(result, list):
        result = [scale * x + centroid[None, :] for x in result]
    else:
        result = scale * result + centroid[None, :]
    return result


def nricp_amberg(source_mesh,
                 target_geometry,
                 source_landmarks=None,
                 target_positions=None,
                 steps=None,
                 eps=0.0001,
                 gamma=1,
                 distance_threshold=0.1,
                 return_records=False,
                 use_faces=True,
                 use_vertex_normals=True,
                 neighbors_count=8):
    """
    Non Rigid Iterative Closest Points

    Implementation of "Amberg et al. 2007: Optimal Step
    Nonrigid ICP Algorithms for Surface Registration."
    Allows to register non-rigidly a mesh on another or
    on a point cloud. The core algorithm is explained
    at the end of page 3 of the paper.

    Comparison between nricp_amberg and nricp_sumner:
    * nricp_amberg fits to the target mesh in less steps
    * nricp_amberg can generate sharp edges
      * only vertices and their neighbors are considered
    * nricp_sumner tend to preserve more the original shape
    * nricp_sumner parameters are easier to tune
    * nricp_sumner solves for triangle positions whereas
      nricp_amberg solves for vertex transforms
    * nricp_sumner is less optimized when wn > 0

    Parameters
    ----------
    source_mesh : Trimesh
        Source mesh containing both vertices and faces.
    target_geometry : Trimesh or PointCloud or (n, 3) float
        Target geometry. It can contain no faces or be a PointCloud.
    source_landmarks : (n,) int or ((n,) int, (n, 3) float)
        n landmarks on the the source mesh.
        Represented as vertex indices (n,) int.
        It can also be represented as a tuple of triangle
        indices and barycentric coordinates ((n,) int, (n, 3) float,).
    target_positions : (n, 3) float
        Target positions assigned to source landmarks
    steps : Core parameters of the algorithm
        Iterable of iterables (ws, wl, wn, max_iter,).
        ws is smoothness term, wl weights landmark importance, wn normal importance
        and max_iter is the maximum number of iterations per step.
    eps : float
        If the error decrease if inferior to this value, the current step ends.
    gamma : float
        Weight the translation part against the rotational/skew part.
        Recommended value : 1.
    distance_threshold : float
        Distance threshold to account for a vertex match or not.
    return_records : bool
        If True, also returns all the intermediate results. It can help debugging
        and tune the parameters to match a specific case.
    use_faces : bool
        If True and if target geometry has faces, use proximity.closest_point to find
        matching points. Else use scipy's cKDTree object.
    use_vertex_normals : bool
        If True and if target geometry has faces, interpolate the normals of the target
        geometry matching points.
        Else use face normals or estimated normals if target geometry has no faces.
    neighbors_count : int
        number of neighbors used for normal estimation. Only used if target geometry has
        no faces or if use_faces is False.

    Returns
    ----------
    result : (n, 3) float or List[(n, 3) float]
        The vertices positions of source_mesh such that it is registered non-rigidly
        onto the target geometry.
        If return_records is True, it returns the list of the vertex positions at each
        iteration.
    """

    def _solve_system(M_kron_G, D, vertices_weight,
                      nearest, ws, nE, nV, Dl, Ul, wl):
        # Solve for Eq. 12
        U = nearest * vertices_weight[:, None]
        use_landmarks = Dl is not None and Ul is not None
        A_stack = [ws * M_kron_G, D.multiply(vertices_weight[:, None])]
        B_shape = (4 * nE + nV, 3)
        if use_landmarks:
            A_stack.append(wl * Dl)
            B_shape = (4 * nE + nV + Ul.shape[0], 3)
        A = sparse.csr_matrix(sparse.vstack(A_stack))
        B = sparse.lil_matrix(B_shape, dtype=np.float32)
        B[4 * nE: (4 * nE + nV), :] = U
        if use_landmarks:
            B[4 * nE + nV: (4 * nE + nV + Ul.shape[0]), :] = Ul * wl
        X = sparse.linalg.spsolve(A.T * A, A.T * B).toarray()
        return X

    def _node_arc_incidence(mesh, do_weight):
        # Computes node-arc incidence matrix of mesh (Eq.10)
        nV = mesh.edges.max() + 1
        nE = len(mesh.edges)
        rows = np.repeat(np.arange(nE), 2)
        cols = mesh.edges.flatten()
        data = np.ones(2 * nE, np.float32)
        data[1::2] = -1
        if do_weight:
            edge_lengths = np.linalg.norm(mesh.vertices[mesh.edges[:, 0]] -
                                          mesh.vertices[mesh.edges[:, 1]], axis=-1)
            data *= np.repeat(1 / edge_lengths, 2)
        return sparse.coo_matrix((data, (rows, cols)), shape=(nE, nV))

    def _create_D(vertex_3d_data):
        # Create Data matrix (Eq. 8)
        nV = len(vertex_3d_data)
        rows = np.repeat(np.arange(nV), 4)
        cols = np.arange(4 * nV)
        data = np.concatenate(
            (vertex_3d_data, np.ones(
                (nV, 1))), axis=-1).flatten()
        return sparse.csr_matrix((data, (rows, cols)), shape=(nV, 4 * nV))

    def _create_X(nV):
        # Create Unknowns Matrix (Eq. 1)
        X_ = np.concatenate((np.eye(3), np.array([[0, 0, 0]])), axis=0)
        return np.tile(X_, (nV, 1))

    def _create_Dl_Ul(D,
                      source_mesh,
                      source_landmarks,
                      target_positions):
        # Create landmark terms (Eq. 11)
        Dl, Ul = None, None

        if source_landmarks is None or target_positions is None:
            # If no landmarks are provided, return None for both
            return Dl, Ul

        if isinstance(source_landmarks, tuple):
            source_tids, source_barys = source_landmarks
            source_tri_vids = source_mesh.faces[source_tids]
            # u * x1, v * x2 and w * x3 combined
            Dl = D[source_tri_vids.flatten(), :]
            Dl.data *= source_barys.flatten().repeat(np.diff(Dl.indptr))
            x0 = source_mesh.vertices[source_tri_vids[:, 0]]
            x1 = source_mesh.vertices[source_tri_vids[:, 1]]
            x2 = source_mesh.vertices[source_tri_vids[:, 2]]
            Ul0 = target_positions - x1 * source_barys[:, 1, None] \
                - x2 * source_barys[:, 2, None]
            Ul1 = target_positions - x0 * source_barys[:, 0, None] \
                - x2 * source_barys[:, 2, None]
            Ul2 = target_positions - x0 * source_barys[:, 0, None] \
                - x1 * source_barys[:, 1, None]
            Ul = np.zeros((Ul0.shape[0] * 3, 3))
            Ul[0::3] = Ul0  # y - v * x2 + w * x3
            Ul[1::3] = Ul1  # y - u * x1 + w * x3
            Ul[2::3] = Ul2  # y - u * x1 + v * x2
        else:
            Dl = D[source_landmarks, :]
            Ul = target_positions
        return Dl, Ul

    target_geometry, target_positions, centroid, scale = \
        _normalize_by_source(source_mesh, target_geometry, target_positions)

    # Number of edges and vertices in source mesh
    nE = len(source_mesh.edges)
    nV = len(source_mesh.vertices)

    # Initialize transformed vertices
    transformed_vertices = source_mesh.vertices.copy()
    # Node-arc incidence (M in Eq. 10)
    M = _node_arc_incidence(source_mesh, True)
    # G (Eq. 10)
    G = np.diag([1, 1, 1, gamma])
    # M kronecker G (Eq. 10)
    M_kron_G = sparse.kron(M, G)
    # D (Eq. 8)
    D = _create_D(source_mesh.vertices)
    # D but for normal computation from the transformations X
    DN = _create_D(source_mesh.vertex_normals)
    # Unknowns 4x3 transformations X (Eq. 1)
    X = _create_X(nV)
    # Landmark related terms (Eq. 11)
    Dl, Ul = _create_Dl_Ul(D, source_mesh, source_landmarks, target_positions)

    # Parameters of the algorithm (Eq. 6)
    # order : Alpha, Beta, normal weighting, and max iteration for step
    if steps is None:
        steps = [
            [0.01, 10, 0.5, 10],
            [0.02, 5, 0.5, 10],
            [0.03, 2.5, 0.5, 10],
            [0.01, 0, 0.0, 10],
        ]
    if return_records:
        records = [transformed_vertices]

    # Main loop
    for ws, wl, wn, max_iter in steps:

        # If normals are estimated from points and if there are less
        # than 3 points per query, avoid normal estimation
        if not use_faces and neighbors_count < 3:
            wn = 0

        last_error = np.finfo(np.float32).max
        error = np.finfo(np.float16).max
        cpt_iter = 0

        # Current step iterations loop
        while last_error - \
                error > eps and (max_iter is None or cpt_iter < max_iter):

            qres = _from_mesh(
                target_geometry,
                transformed_vertices,
                from_vertices_only=not use_faces,
                return_normals=wn > 0,
                return_interpolated_normals=wn > 0 and use_vertex_normals,
                neighbors_count=neighbors_count)

            # Data weighting
            vertices_weight = np.ones(nV)
            vertices_weight[qres['distances'] > distance_threshold] = 0

            if wn > 0 and 'normals' in qres:
                target_normals = qres['normals']
                if use_vertex_normals and 'interpolated_normals' in qres:
                    target_normals = qres['interpolated_normals']
                # Normal weighting = multiplying weights by cosines^wn
                source_normals = DN * X
                dot = util.diagonal_dot(source_normals, target_normals)
                # Normal orientation is only known for meshes as target
                dot = np.clip(dot, 0, 1) if use_faces else np.abs(dot)
                vertices_weight = vertices_weight * dot ** wn

            # Actual system solve
            X = _solve_system(M_kron_G, D, vertices_weight, qres['nearest'],
                              ws, nE, nV, Dl, Ul, wl)
            transformed_vertices = D * X
            last_error = error
            error_vec = np.linalg.norm(
                qres['nearest'] - transformed_vertices, axis=-1)
            error = (error_vec * vertices_weight).mean()
            if return_records:
                records.append(transformed_vertices)
            cpt_iter += 1

    if return_records:
        result = records
    else:
        result = transformed_vertices

    result = _denormalize_by_source(source_mesh, target_geometry, target_positions,
                                    result, centroid, scale)
    return result


def _from_mesh(mesh,
               input_points,
               from_vertices_only=False,
               return_barycentric_coordinates=False,
               return_normals=False,
               return_interpolated_normals=False,
               neighbors_count=10,
               **kwargs):
    """
    Find the the closest points and associated attributes from a Trimesh.

    Parameters
    -----------
    mesh : Trimesh
        Trimesh from which the query is performed
    input_points : (m, 3) float
        Input query points
    from_vertices_only : bool
        If True, consider only the vertices and not the faces
    return_barycentric_coordinates : bool
        If True, return the barycentric coordinates
    return_normals : bool
        If True, compute the normals at each closest point
    return_interpolated_normals : bool
        If True, return the interpolated normal at each closest point
    neighbors_count : int
        The number of closest neighbors to query
    kwargs : dict
        Dict to accept other key word arguments (not used)
    Returns
    ----------
    qres : Dict
      Dictionary containing :
       - nearest points (m, 3) with key 'nearest'
       - distances to nearest point (m,) with key 'distances'
       - support triangle indices of the nearest points (m,) with key 'tids'
       - [optional] normals at nearest points (m,3) with key 'normals'
       - [optional] barycentric coordinates in support triangles (m,3) with key
         'barycentric_coordinates'
       - [optional] interpolated normals (m,3) with key 'interpolated_normals'
    """
    input_points = np.asanyarray(input_points)
    neighbors_count = min(neighbors_count, len(mesh.vertices))

    if from_vertices_only or len(mesh.faces) == 0:
        # Consider only the vertices
        return _from_points(
            mesh.vertices, input_points, mesh.kdtree,
            return_normals=return_normals,
            neighbors_count=neighbors_count)
    # Else if we consider faces, use proximity.closest_point
    qres = {}
    from .proximity import closest_point
    from .triangles import points_to_barycentric
    qres['nearest'], qres['distances'], qres['tids'] = closest_point(
        mesh, input_points)

    if return_normals:
        qres['normals'] = mesh.face_normals[qres['tids']]
    if return_barycentric_coordinates or return_interpolated_normals:
        qres['barycentric_coordinates'] = points_to_barycentric(
            mesh.vertices[mesh.faces[qres['tids']]], qres['nearest'])

        if return_interpolated_normals:
            # Interpolation from barycentric coordinates
            qres['interpolated_normals'] = \
                np.einsum('ij,ijk->ik',
                          qres['barycentric_coordinates'],
                          mesh.vertex_normals[mesh.faces[qres['tids']]])
    return qres


def _from_points(target_points,
                 input_points,
                 kdtree=None,
                 return_normals=False,
                 neighbors_count=10,
                 **kwargs):
    """
    Find the the closest points and associated attributes
    from a set of 3D points.

    Parameters
    -----------
    target_points : (n, 3) float
      Points from which the query is performed
    input_points : (m, 3) float
      Input query points
    kdtree : scipy.cKDTree
      KDTree used for query. Computed if not provided
    return_normals : bool
      If True, compute the normals at each nearest point
    neighbors_count : int
      The number of closest neighbors to query
    kwargs : dict
      Dict to accept other key word arguments (not used)

    Returns
    ----------
    qres : Dict
      Dictionary containing :
       - nearest points (m, 3) with key 'nearest'
       - distances to nearest point (m,) with key 'distances'
       - vertex indices of the nearest points (m,) with key 'vertex_indices'
       - [optional] normals at nearest points (m,3) with key 'normals'
    """
    # Empty result
    target_points = np.asanyarray(target_points)
    input_points = np.asanyarray(input_points)
    neighbors_count = min(neighbors_count, len(target_points))
    qres = {}
    if kdtree is None:
        kdtree = cKDTree(target_points)

    if return_normals:
        assert neighbors_count >= 3
        distances, indices = kdtree.query(
            input_points, k=neighbors_count)
        nearest = target_points[indices, :]
        qres['normals'] = plane_fit(nearest)[1]
        qres['nearest'] = nearest[:, 0]
        qres['distances'] = distances[:, 0]
        qres['vertex_indices'] = indices[:, 0]
    else:
        qres['distances'], qres['vertex_indices'] = kdtree.query(
            input_points)
        qres['nearest'] = target_points[qres['vertex_indices'], :]

    return qres


def nricp_sumner(source_mesh,
                 target_geometry,
                 source_landmarks=None,
                 target_positions=None,
                 steps=None,
                 distance_threshold=0.1,
                 return_records=False,
                 use_faces=True,
                 use_vertex_normals=True,
                 neighbors_count=8,
                 face_pairs_type='vertex'):
    """
    Non Rigid Iterative Closest Points

    Implementation of the correspondence computation part of
    "Sumner and Popovic 2004: Deformation Transfer for Triangle Meshes"
    Allows to register non-rigidly a mesh on another geometry.

    Comparison between nricp_amberg and nricp_sumner:
    * nricp_amberg fits to the target mesh in less steps
    * nricp_amberg can generate sharp edges (only vertices and their
        neighbors are considered)
    * nricp_sumner tend to preserve more the original shape
    * nricp_sumner parameters are easier to tune
    * nricp_sumner solves for triangle positions whereas nricp_amberg solves for
        vertex transforms
    * nricp_sumner is less optimized when wn > 0

    Parameters
    ----------
    source_mesh : Trimesh
        Source mesh containing both vertices and faces.
    target_geometry : Trimesh or PointCloud or (n, 3) float
        Target geometry. It can contain no faces or be a PointCloud.
    source_landmarks : (n,) int or ((n,) int, (n, 3) float)
        n landmarks on the the source mesh.
        Represented as vertex indices (n,) int.
        It can also be represented as a tuple of triangle indices and barycentric
        coordinates ((n,) int, (n, 3) float,).
    target_positions : (n, 3) float
        Target positions assigned to source landmarks
    steps : Core parameters of the algorithm
        Iterable of iterables (wc, wi, ws, wl, wn).
        wc is the correspondence term (strength of fitting), wi is the identity term
        (recommended value : 0.001), ws is smoothness term, wl weights the landmark
        importance and wn the normal importance.
    distance_threshold : float
        Distance threshold to account for a vertex match or not.
    return_records : bool
        If True, also returns all the intermediate results. It can help debugging
        and tune the parameters to match a specific case.
    use_faces : bool
        If True and if target geometry has faces, use proximity.closest_point to find
        matching points. Else use scipy's cKDTree object.
    use_vertex_normals : bool
        If True and if target geometry has faces, interpolate the normals of the target
        geometry matching points.
        Else use face normals or estimated normals if target geometry has no faces.
    neighbors_count : int
        number of neighbors used for normal estimation. Only used if target geometry has
        no faces or if use_faces is False.
    face_pairs_type : str 'vertex' or 'edge'
        Method to determine face pairs used in the smoothness cost. 'vertex' yields
        smoother results.


    Returns
    ----------
    result : (n, 3) float or List[(n, 3) float]
        The vertices positions of source_mesh such that it is registered non-rigidly
        onto the target geometry.
        If return_records is True, it returns the list of the vertex positions at each
        iteration.
    """

    def _construct_transform_matrix(faces, Vinv, size):
        # Utility function for constructing the per-frame transforms
        _construct_transform_matrix._row = np.array([0, 1, 2] * 4)
        nV = len(Vinv)
        rows = np.tile(_construct_transform_matrix._row, nV) \
            + 3 * np.repeat(np.arange(nV), 12)
        cols = np.repeat(faces.flat, 3)
        minus_inv_sum = -Vinv.sum(axis=1)
        Vinv_flat = Vinv.reshape(nV, 9)
        data = np.concatenate((minus_inv_sum, Vinv_flat), axis=-1).flatten()
        return sparse.coo_matrix((data, (rows, cols)),
                                 shape=(3 * nV, size), dtype=float)

    def _build_tetrahedrons(mesh):
        # UUtility function for constructing the frames
        v4_vec = mesh.face_normals
        v1 = mesh.triangles[:, 0]
        v2 = mesh.triangles[:, 1]
        v3 = mesh.triangles[:, 2]
        v4 = v1 + v4_vec
        vertices = np.concatenate((mesh.vertices, v4))
        nV, nT = len(mesh.vertices), len(mesh.faces)
        v4_indices = np.arange(nV, nV + nT)[:, None]
        tetrahedrons = np.concatenate((mesh.faces, v4_indices), axis=-1)
        frames = np.concatenate(((v2 - v1)[..., None],
                                (v3 - v1)[..., None],
                                v4_vec[..., None]), axis=-1)
        return vertices, tetrahedrons, frames

    def _construct_identity_cost(vtet, tet, Vinv):
        # Utility function for constructing the identity cost
        AEi = _construct_transform_matrix(tet, Vinv, len(vtet),).tocsr()
        Bi = np.tile(np.identity(3, dtype=float), (len(tet), 1))
        return AEi, Bi

    def _construct_smoothness_cost(vtet, tet, Vinv, face_pairs):
        # Utility function for constructing the smoothness (stiffness) cost
        AEs_r = _construct_transform_matrix(tet[face_pairs[:, 0]],
                                            Vinv[face_pairs[:, 0]],
                                            len(vtet)).tocsr()
        AEs_l = _construct_transform_matrix(tet[face_pairs[:, 1]],
                                            Vinv[face_pairs[:, 1]],
                                            len(vtet)).tocsr()
        AEs = (AEs_r - AEs_l).tocsc()
        AEs.eliminate_zeros()
        Bs = np.zeros((len(face_pairs) * 3, 3))
        return AEs, Bs

    def _construct_landmark_cost(vtet, source_mesh, source_landmarks):
        # Utility function for constructing the landmark cost
        if source_landmarks is None:
            return None, np.ones(len(source_mesh.vertices), dtype=bool)
        if isinstance(source_landmarks, tuple):
            # If the input source landmarks are in barycentric form
            source_landmarks_tids, source_landmarks_barys = source_landmarks
            source_landmarks_vids = source_mesh.faces[source_landmarks_tids]
            nL, nVT = len(source_landmarks_tids), len(vtet)

            rows = np.repeat(np.arange(nL), 3)
            cols = source_landmarks_vids.flat
            data = source_landmarks_barys.flat

            AEl = sparse.coo_matrix((data, (rows, cols)), shape=(nL, nVT))
            marker_vids = \
                source_landmarks_vids[source_landmarks_barys > np.finfo(
                    np.float16).eps]
            non_markers_mask = np.ones(len(source_mesh.vertices), dtype=bool)
            non_markers_mask[marker_vids] = False
        else:
            # Else if they are in vertex index form
            nL, nVT = len(source_landmarks), len(vtet)
            rows = np.arange(nL)
            cols = source_landmarks.flat
            data = np.ones(nL)
            AEl = sparse.coo_matrix((data, (rows, cols)), shape=(nL, nVT))
            non_markers_mask = np.ones(len(source_mesh.vertices), dtype=bool)
            non_markers_mask[source_landmarks] = False
        return AEl, non_markers_mask

    def _construct_correspondence_cost(points, non_markers_mask, size):
        # Utility function for constructing the correspondence cost
        AEc = sparse.identity(
            size, dtype=float, format="csc")[
            :len(non_markers_mask)]
        AEc = AEc[non_markers_mask]
        Bc = points[non_markers_mask]
        return AEc, Bc

    def _compute_vertex_normals(vertices, faces):
        # Utility function for computing source vertex normals
        mesh_triangles = vertices[faces]
        mesh_triangles_cross = cross(mesh_triangles)
        mesh_face_normals = normals(
            triangles=mesh_triangles,
            crosses=mesh_triangles_cross)[0]
        mesh_face_angles = angles(mesh_triangles)
        mesh_normals = weighted_vertex_normals(
            vertex_count=nV,
            faces=faces,
            face_normals=mesh_face_normals,
            face_angles=mesh_face_angles)
        return mesh_normals

    # First, normalize the source and target to [-1, 1]^3
    (target_geometry,
     target_positions,
     centroid,
     scale) = _normalize_by_source(
         source_mesh, target_geometry, target_positions)
    nV = len(source_mesh.vertices)
    use_landmarks = source_landmarks is not None and target_positions is not None

    if steps is None:
        steps = [
            # [wc, wi, ws, wl, wn],
            [1, 0.001, 1.0, 1000, 0],
            [1, 0.001, 1.0, 1000, 0],
            [10, 0.001, 1.0, 1000, 0],
            [100, 0.001, 1.0, 1000, 0],
        ]

    source_vtet, source_tet, V = _build_tetrahedrons(source_mesh)
    Vinv = np.linalg.inv(V)

    # List of (n, 2) faces index which share a vertex
    if face_pairs_type == 'vertex':
        face_pairs = source_mesh.face_neighborhood
    else:
        face_pairs = source_mesh.face_adjacency

    # Construct the cost matrices
    # Identity cost (Eq. 12)
    AEi, Bi = _construct_identity_cost(source_vtet, source_tet, Vinv)
    # Smoothness cost (Eq. 11)
    AEs, Bs = _construct_smoothness_cost(source_vtet, source_tet,
                                         Vinv, face_pairs)
    # Landmark cost (Eq. 13)
    AEl, non_markers_mask = _construct_landmark_cost(source_vtet, source_mesh,
                                                     source_landmarks)

    transformed_vertices = source_vtet.copy()
    if return_records:
        records = [transformed_vertices[:nV]]

    # Main loop
    for i, (wc, wi, ws, wl, wn) in enumerate(steps):

        Astack = [AEi * wi, AEs * ws]
        Bstack = [Bi * wi, Bs * ws]

        if use_landmarks:
            Astack.append(AEl * wl)
            Bstack.append(target_positions * wl)

        if (i > 0 or not use_landmarks) and wc > 0:
            # Query the nearest points
            qres = _from_mesh(
                target_geometry,
                transformed_vertices[:nV],
                from_vertices_only=not use_faces,
                return_normals=wn > 0,
                return_interpolated_normals=(
                    use_vertex_normals and wn > 0),
                neighbors_count=neighbors_count)

            # Correspondence cost (Eq. 13)
            AEc, Bc = _construct_correspondence_cost(
                qres['nearest'],
                non_markers_mask,
                len(source_vtet))
            vertices_weight = np.ones(nV)
            vertices_weight[qres['distances'] > distance_threshold] = 0
            if wn > 0 or 'normals' in qres:
                target_normals = qres['normals']
                if use_vertex_normals and 'interpolated_normals' in qres:
                    target_normals = qres['interpolated_normals']
                # Normal weighting : multiplying weights by cosines^wn
                source_normals = _compute_vertex_normals(transformed_vertices,
                                                         source_mesh.faces)
                dot = util.diagonal_dot(source_normals, target_normals)
                # Normal orientation is only known for meshes as target
                dot = np.clip(dot, 0, 1) if use_faces else np.abs(dot)
                vertices_weight = vertices_weight * dot ** wn

            # Account for vertices' weight
            AEc.data *= vertices_weight[non_markers_mask][AEc.indices]
            Bc *= vertices_weight[non_markers_mask, None]

            Astack.append(AEc * wc)
            Bstack.append(Bc * wc)

        # Now solve Eq. 14 ...
        A = sparse.vstack(Astack, format="csc")
        A.eliminate_zeros()
        b = np.concatenate(Bstack)

        LU = sparse.linalg.splu((A.T * A).tocsc())
        transformed_vertices = LU.solve(A.T * b)
        # done !

        if return_records:
            records.append(transformed_vertices[:nV])

    if return_records:
        result = records
    else:
        result = transformed_vertices[:nV]

    result = _denormalize_by_source(
        source_mesh,
        target_geometry,
        target_positions,
        result,
        centroid,
        scale)
    return result


# trimesh/remesh.py

"""
remesh.py
-------------

Deal with re- triangulation of existing meshes.
"""
import numpy as np

from . import util
from . import grouping
from . import graph

from .geometry import faces_to_edges
from .constants import tol


def subdivide(vertices,
              faces,
              face_index=None,
              vertex_attributes=None,
              return_index=False):
    """
    Subdivide a mesh into smaller triangles.

    Note that if `face_index` is passed, only those
    faces will be subdivided and their neighbors won't
    be modified making the mesh no longer "watertight."

    Parameters
    ------------
    vertices : (n, 3) float
      Vertices in space
    faces : (m, 3) int
      Indexes of vertices which make up triangular faces
    face_index : faces to subdivide.
      if None: all faces of mesh will be subdivided
      if (n,) int array of indices: only specified faces
    vertex_attributes : dict
      Contains (n, d) attribute data
    return_index : bool
      If True, return index of original face for new faces

    Returns
    ----------
    new_vertices : (q, 3) float
      Vertices in space
    new_faces : (p, 3) int
      Remeshed faces
    index_dict : dict
      Only returned if `return_index`, {index of
      original face : index of new faces}.
    """
    if face_index is None:
        face_mask = np.ones(len(faces), dtype=bool)
    else:
        face_mask = np.zeros(len(faces), dtype=bool)
        face_mask[face_index] = True

    # the (c, 3) int array of vertex indices
    faces_subset = faces[face_mask]

    # find the unique edges of our faces subset
    edges = np.sort(faces_to_edges(faces_subset), axis=1)
    unique, inverse = grouping.unique_rows(edges)
    # then only produce one midpoint per unique edge
    mid = vertices[edges[unique]].mean(axis=1)
    mid_idx = inverse.reshape((-1, 3)) + len(vertices)

    # the new faces_subset with correct winding
    f = np.column_stack([faces_subset[:, 0],
                         mid_idx[:, 0],
                         mid_idx[:, 2],
                         mid_idx[:, 0],
                         faces_subset[:, 1],
                         mid_idx[:, 1],
                         mid_idx[:, 2],
                         mid_idx[:, 1],
                         faces_subset[:, 2],
                         mid_idx[:, 0],
                         mid_idx[:, 1],
                         mid_idx[:, 2]]).reshape((-1, 3))

    # add the 3 new faces_subset per old face all on the end
    # by putting all the new faces after all the old faces
    # it makes it easier to understand the indexes
    new_faces = np.vstack((faces[~face_mask], f))
    # stack the new midpoint vertices on the end
    new_vertices = np.vstack((vertices, mid))

    if vertex_attributes is not None:
        new_attributes = {}
        for key, values in vertex_attributes.items():
            attr_tris = values[faces_subset]
            attr_mid = np.vstack([
                attr_tris[:, g, :].mean(axis=1)
                for g in [[0, 1],
                          [1, 2],
                          [2, 0]]])
            attr_mid = attr_mid[unique]
            new_attributes[key] = np.vstack((
                values, attr_mid))
        return new_vertices, new_faces, new_attributes

    if return_index:
        # turn the mask back into integer indexes
        nonzero = np.nonzero(face_mask)[0]
        # new faces start past the original faces
        # but we've removed all the faces in face_mask
        start = len(faces) - len(nonzero)
        # indexes are just offset from start
        stack = np.arange(
            start, start + len(f) * 4).reshape((-1, 4))
        # reformat into a slightly silly dict for some reason
        index_dict = {k: v for k, v in zip(nonzero, stack)}

        return new_vertices, new_faces, index_dict

    return new_vertices, new_faces


def subdivide_to_size(vertices,
                      faces,
                      max_edge,
                      max_iter=10,
                      return_index=False):
    """
    Subdivide a mesh until every edge is shorter than a
    specified length.

    Will return a triangle soup, not a nicely structured mesh.

    Parameters
    ------------
    vertices : (n, 3) float
      Vertices in space
    faces : (m, 3) int
      Indices of vertices which make up triangles
    max_edge : float
      Maximum length of any edge in the result
    max_iter : int
      The maximum number of times to run subdivision
    return_index : bool
      If True, return index of original face for new faces

    Returns
    ------------
    vertices : (j, 3) float
      Vertices in space
    faces : (q, 3) int
      Indices of vertices
    index : (q, 3) int
      Only returned if `return_index`, index of
      original face for each new face.
    """
    # store completed
    done_face = []
    done_vert = []
    done_idx = []

    # copy inputs and make sure dtype is correct
    current_faces = np.array(
        faces, dtype=np.int64, copy=True)
    current_vertices = np.array(
        vertices, dtype=np.float64, copy=True)

    # store a map to the original face index
    current_index = np.arange(len(faces))

    # loop through iteration cap
    for i in range(max_iter + 1):
        # compute the length of every triangle edge
        edge_length = (np.diff(
            current_vertices[current_faces[:, [0, 1, 2, 0]], :3],
            axis=1) ** 2).sum(axis=2) ** 0.5
        # check edge length against maximum
        too_long = (edge_length > max_edge).any(axis=1)
        # faces that are OK
        face_ok = ~too_long

        # clean up the faces a little bit so we don't
        # store a ton of unused vertices
        unique, inverse = grouping.unique_bincount(
            current_faces[face_ok].flatten(),
            return_inverse=True)

        # store vertices and faces meeting criteria
        done_vert.append(current_vertices[unique])
        done_face.append(inverse.reshape((-1, 3)))

        if return_index:
            done_idx.append(current_index[face_ok])
            current_index = np.tile(current_index[too_long],
                                    (4, 1)).T.ravel()

        # met our goals so exit
        if not too_long.any():
            break

        # check max_iter before subdividing again
        if i >= max_iter:
            raise ValueError('max_iter exceeded!')

        # run subdivision again
        (current_vertices,
         current_faces) = subdivide(current_vertices,
                                    current_faces[too_long])

    # stack sequence into nice (n, 3) arrays
    final_vertices, final_faces = util.append_faces(
        done_vert, done_face)

    if return_index:
        final_index = np.concatenate(done_idx)
        assert len(final_index) == len(final_faces)
        return final_vertices, final_faces, final_index

    return final_vertices, final_faces


def subdivide_loop(vertices,
                   faces,
                   iterations=None):
    """
    Subdivide a mesh by dividing each triangle into four triangles
    and approximating their smoothed surface (loop subdivision).
    This function is an array-based implementation of loop subdivision,
    which avoids slow for loop and enables faster calculation.

    Overall process:
    1. Calculate odd vertices.
      Assign a new odd vertex on each edge and
      calculate the value for the boundary case and the interior case.
      The value is calculated as follows.
          v2
        / f0 \\        0
      v0--e--v1      /   \\
        \\f1 /     v0--e--v1
          v3
      - interior case : 3:1 ratio of mean(v0,v1) and mean(v2,v3)
      - boundary case : mean(v0,v1)
    2. Calculate even vertices.
      The new even vertices are calculated with the existing
      vertices and their adjacent vertices.
        1---2
       / \\/ \\      0---1
      0---v---3     / \\/ \\
       \\ /\\/    b0---v---b1
        k...4
      - interior case : (1-kB):B ratio of v and k adjacencies
      - boundary case : 3:1 ratio of v and mean(b0,b1)
    3. Compose new faces with new vertices.

    Parameters
    ------------
    vertices : (n, 3) float
      Vertices in space
    faces : (m, 3) int
      Indices of vertices which make up triangles

    Returns
    ------------
    vertices : (j, 3) float
      Vertices in space
    faces : (q, 3) int
      Indices of vertices
    iterations : int
          Number of iterations to run subdivision
    """
    try:
        from itertools import zip_longest
    except BaseException:
        # python2
        from itertools import izip_longest as zip_longest

    if iterations is None:
        iterations = 1

    def _subdivide(vertices, faces):
        # find the unique edges of our faces
        edges, edges_face = faces_to_edges(
            faces, return_index=True)
        edges.sort(axis=1)
        unique, inverse = grouping.unique_rows(edges)

        # set interior edges if there are two edges and boundary if there is
        # one.
        edge_inter = np.sort(
            grouping.group_rows(
                edges,
                require_count=2),
            axis=1)
        edge_bound = grouping.group_rows(edges, require_count=1)
        # make sure that one edge is shared by only one or two faces.
        if not len(edge_inter) * 2 + len(edge_bound) == len(edges):
            # we have multiple bodies it's a party!
            # edges shared by 2 faces are "connected"
            # so this connected components operation is
            # essentially identical to `face_adjacency`
            faces_group = graph.connected_components(
                edges_face[edge_inter])

            if len(faces_group) == 1:
                raise ValueError('Some edges are shared by more than 2 faces')

            # collect a subdivided copy of each body
            seq_verts = []
            seq_faces = []
            # keep track of vertex count as we go so
            # we can do a single vstack at the end
            count = 0
            # loop through original face indexes
            for f in faces_group:
                # a lot of the complexity in this operation
                # is computing vertex neighbors so we only
                # want to pass forward the referenced vertices
                # for this particular group of connected faces
                unique, inverse = grouping.unique_bincount(
                    faces[f].reshape(-1), return_inverse=True)

                # subdivide this subset of faces
                cur_verts, cur_faces = _subdivide(
                    vertices=vertices[unique],
                    faces=inverse.reshape((-1, 3)))

                # increment the face references to match
                # the vertices when we stack them later
                cur_faces += count
                # increment the total vertex count
                count += len(cur_verts)
                # append to the sequence
                seq_verts.append(cur_verts)
                seq_faces.append(cur_faces)

            # return results as clean (n, 3) arrays
            return np.vstack(seq_verts), np.vstack(seq_faces)

        # set interior, boundary mask for unique edges
        edge_bound_mask = np.zeros(len(edges), dtype=bool)
        edge_bound_mask[edge_bound] = True
        edge_bound_mask = edge_bound_mask[unique]
        edge_inter_mask = ~edge_bound_mask

        # find the opposite face for each edge
        edge_pair = np.zeros(len(edges)).astype(int)
        edge_pair[edge_inter[:, 0]] = edge_inter[:, 1]
        edge_pair[edge_inter[:, 1]] = edge_inter[:, 0]
        opposite_face1 = edges_face[unique]
        opposite_face2 = edges_face[edge_pair[unique]]

        # set odd vertices to the middle of each edge (default as boundary
        # case).
        odd = vertices[edges[unique]].mean(axis=1)
        # modify the odd vertices for the interior case
        e = edges[unique[edge_inter_mask]]
        e_v0 = vertices[e][:, 0]
        e_v1 = vertices[e][:, 1]
        e_f0 = faces[opposite_face1[edge_inter_mask]]
        e_f1 = faces[opposite_face2[edge_inter_mask]]
        e_v2_idx = e_f0[~(e_f0[:, :, None] == e[:, None, :]).any(-1)]
        e_v3_idx = e_f1[~(e_f1[:, :, None] == e[:, None, :]).any(-1)]
        e_v2 = vertices[e_v2_idx]
        e_v3 = vertices[e_v3_idx]

        # simplified from:
        # # 3 / 8 * (e_v0 + e_v1) + 1 / 8 * (e_v2 + e_v3)
        odd[edge_inter_mask] = 0.375 * e_v0 + \
            0.375 * e_v1 + e_v2 / 8.0 + e_v3 / 8.0

        # find vertex neighbors of each vertex
        neighbors = graph.neighbors(
            edges=edges[unique],
            max_index=len(vertices))
        # convert list type of array into a fixed-shaped numpy array (set -1 to
        # empties)
        neighbors = np.array(list(zip_longest(*neighbors, fillvalue=-1))).T
        # if the neighbor has -1 index, its point is (0, 0, 0), so that
        # it is not included in the summation of neighbors when calculating the
        # even
        vertices_ = np.vstack([vertices, [0.0, 0.0, 0.0]])
        # number of neighbors
        k = (neighbors + 1).astype(bool).sum(axis=1)

        # calculate even vertices for the interior case
        even = np.zeros_like(vertices)

        # beta = 1 / k * (5 / 8 - (3 / 8 + 1 / 4 * np.cos(2 * np.pi / k)) ** 2)
        # simplified with sympy.parse_expr('...').simplify()
        beta = (40.0 - (2.0 * np.cos(2 * np.pi / k) + 3)**2) / (64 * k)
        even = beta[:, None] * vertices_[neighbors].sum(1) \
            + (1 - k[:, None] * beta[:, None]) * vertices

        # calculate even vertices for the boundary case
        if edge_bound_mask.any():
            # boundary vertices from boundary edges
            vrt_bound_mask = np.zeros(len(vertices), dtype=bool)
            vrt_bound_mask[np.unique(edges[unique][~edge_inter_mask])] = True
            # one boundary vertex has two neighbor boundary vertices (set
            # others as -1)
            boundary_neighbors = neighbors[vrt_bound_mask]
            boundary_neighbors[~vrt_bound_mask[neighbors[vrt_bound_mask]]] = -1

            even[vrt_bound_mask] = (vertices_[boundary_neighbors].sum(axis=1) / 8.0 +
                                    (3.0 / 4.0) * vertices[vrt_bound_mask])

        # the new faces with odd vertices
        odd_idx = inverse.reshape((-1, 3)) + len(vertices)
        new_faces = np.column_stack([
            faces[:, 0],
            odd_idx[:, 0],
            odd_idx[:, 2],
            odd_idx[:, 0],
            faces[:, 1],
            odd_idx[:, 1],
            odd_idx[:, 2],
            odd_idx[:, 1],
            faces[:, 2],
            odd_idx[:, 0],
            odd_idx[:, 1],
            odd_idx[:, 2]]).reshape((-1, 3))

        # stack the new even vertices and odd vertices
        new_vertices = np.vstack((even, odd))

        return new_vertices, new_faces

    for _ in range(iterations):
        vertices, faces = _subdivide(vertices, faces)

    if tol.strict or True:
        assert np.isfinite(vertices).all()
        assert np.isfinite(faces).all()
        # should raise if faces are malformed
        assert np.isfinite(vertices[faces]).all()

        # none of the faces returned should be degenerate
        # i.e. every face should have 3 unique vertices
        assert (faces[:, 1:] != faces[:, :1]).all()

    return vertices, faces


# trimesh/rendering.py

"""
rendering.py
--------------

Functions to convert trimesh objects to pyglet/opengl objects.
"""

import numpy as np

from . import util

# avoid importing pyglet or pyglet.gl
# as pyglet does things on import
GL_POINTS, GL_LINES, GL_TRIANGLES = (0, 1, 4)


def convert_to_vertexlist(geometry, **kwargs):
    """
    Try to convert various geometry objects to the constructor
    args for a pyglet indexed vertex list.

    Parameters
    ------------
    obj : Trimesh, Path2D, Path3D, (n,2) float, (n,3) float
      Object to render

    Returns
    ------------
    args : tuple
      Args to be passed to pyglet indexed vertex list
      constructor.
    """
    if util.is_instance_named(geometry, 'Trimesh'):
        return mesh_to_vertexlist(geometry, **kwargs)
    elif util.is_instance_named(geometry, 'Path'):
        # works for Path3D and Path2D
        # both of which inherit from Path
        return path_to_vertexlist(
            geometry, **kwargs)
    elif util.is_instance_named(geometry, 'PointCloud'):
        # pointcloud objects contain colors
        return points_to_vertexlist(geometry.vertices,
                                    colors=geometry.colors,
                                    **kwargs)
    elif util.is_instance_named(geometry, 'ndarray'):
        # (n,2) or (n,3) points
        return points_to_vertexlist(geometry, **kwargs)
    elif util.is_instance_named(geometry, 'VoxelGrid'):
        # for voxels view them as a bunch of boxes
        return mesh_to_vertexlist(geometry.as_boxes(**kwargs),
                                  **kwargs)
    else:
        raise ValueError('Geometry passed is not a viewable type!')


def mesh_to_vertexlist(mesh,
                       group=None,
                       smooth=True,
                       smooth_threshold=60000):
    """
    Convert a Trimesh object to arguments for an
    indexed vertex list constructor.

    Parameters
    -------------
    mesh : trimesh.Trimesh
      Mesh to be rendered
    group : str
      Rendering group for the vertex list
    smooth : bool
      Should we try to smooth shade the mesh
    smooth_threshold : int
      Maximum number of faces to smooth shade

    Returns
    --------------
    args : (7,) tuple
      Args for vertex list constructor
    """

    if hasattr(mesh.visual, 'uv'):
        # if the mesh has texture defined pass it to pyglet
        vertex_count = len(mesh.vertices)
        normals = mesh.vertex_normals.reshape(-1).tolist()
        faces = mesh.faces.reshape(-1).tolist()
        vertices = mesh.vertices.reshape(-1).tolist()

        # get the per-vertex UV coordinates
        uv = mesh.visual.uv

        # shortcut for the material
        material = mesh.visual.material
        if hasattr(material, 'image'):
            # does the material actually have an image specified
            no_image = material.image is None
        elif hasattr(material, 'baseColorTexture'):
            no_image = material.baseColorTexture is None
        else:
            no_image = True

        # didn't get valid texture so skip it
        if uv is None or no_image or len(uv) != vertex_count:
            # if no UV coordinates on material, just set face colors
            # to the diffuse color of the material
            color_gl = colors_to_gl(
                material.main_color, vertex_count)
        else:
            # if someone passed (n, 3) UVR cut it off here
            if uv.shape[1] > 2:
                uv = uv[:, :2]
            # texcoord as (2,) float
            color_gl = ('t2f/static',
                        uv.astype(np.float64).reshape(-1).tolist())

    elif smooth and len(mesh.faces) < smooth_threshold:
        # if we have a small number of faces and colors defined
        # smooth the  mesh by merging vertices of faces below
        # the threshold angle
        mesh = mesh.smoothed()
        vertex_count = len(mesh.vertices)
        normals = mesh.vertex_normals.reshape(-1).tolist()
        faces = mesh.faces.reshape(-1).tolist()
        vertices = mesh.vertices.reshape(-1).tolist()
        color_gl = colors_to_gl(mesh.visual.vertex_colors,
                                vertex_count)
    else:
        # we don't have textures or want to smooth so
        # send a polygon soup of disconnected triangles to opengl
        vertex_count = len(mesh.triangles) * 3
        normals = np.tile(mesh.face_normals,
                          (1, 3)).reshape(-1).tolist()
        vertices = mesh.triangles.reshape(-1).tolist()
        faces = np.arange(vertex_count).tolist()
        colors = np.tile(mesh.visual.face_colors,
                         (1, 3)).reshape((-1, 4))
        color_gl = colors_to_gl(colors, vertex_count)

    # create the ordered tuple for pyglet, use like:
    # `batch.add_indexed(*args)`
    args = (vertex_count,    # number of vertices
            GL_TRIANGLES,    # mode
            group,           # group
            faces,           # indices
            ('v3f/static', vertices),
            ('n3f/static', normals),
            color_gl)
    return args


def path_to_vertexlist(path, group=None, **kwargs):
    """
    Convert a Path3D object to arguments for a
    pyglet indexed vertex list constructor.

    Parameters
    -------------
    path : trimesh.path.Path3D object
      Mesh to be rendered
    group : str
      Rendering group for the vertex list

    Returns
    --------------
    args : (7,) tuple
      Args for vertex list constructor
    """
    # avoid cache check inside tight loop
    vertices = path.vertices

    # get (n, 2, (2|3)) lines
    stacked = [util.stack_lines(e.discrete(vertices))
               for e in path.entities]
    lines = util.vstack_empty(stacked)
    count = len(lines)

    # stack zeros for 2D lines
    if util.is_shape(vertices, (-1, 2)):
        lines = lines.reshape((-1, 2))
        lines = np.column_stack((lines, np.zeros(len(lines))))
    # index for GL is one per point
    index = np.arange(count).tolist()
    # convert from entity color to the color of
    # each vertex in the line segments
    colors = path.colors
    if colors is not None:
        colors = np.vstack(
            [(np.ones((len(s), 4)) * c).astype(np.uint8)
             for s, c in zip(stacked, path.colors)])
    # convert to gl-friendly colors
    gl_colors = colors_to_gl(colors, count=count)

    # collect args for vertexlist constructor
    args = (count,    # number of lines
            GL_LINES,  # mode
            group,    # group
            index,    # indices
            ('v3f/static', lines.reshape(-1)),
            gl_colors)
    return args


def points_to_vertexlist(points,
                         colors=None,
                         group=None,
                         **kwargs):
    """
    Convert a numpy array of 3D points to args for
    a vertex list constructor.

    Parameters
    -------------
    points : (n, 3) float
      Points to be rendered
    colors : (n, 3) or (n, 4) float
      Colors for each point
    group : str
      Rendering group for the vertex list

    Returns
    --------------
    args : (7,) tuple
      Args for vertex list constructor
    """
    points = np.asanyarray(points, dtype=np.float64)

    if util.is_shape(points, (-1, 2)):
        points = np.column_stack((points, np.zeros(len(points))))
    elif not util.is_shape(points, (-1, 3)):
        raise ValueError('Pointcloud must be (n,3)!')

    index = np.arange(len(points)).tolist()

    args = (len(points),  # number of vertices
            GL_POINTS,   # mode
            group,       # group
            index,       # indices
            ('v3f/static', points.reshape(-1)),
            colors_to_gl(colors, len(points)))
    return args


def colors_to_gl(colors, count):
    """
    Given a list of colors (or None) return a GL-acceptable
    list of colors.

    Parameters
    ------------
    colors: (count, (3 or 4)) float
      Input colors as an array

    Returns
    ---------
    colors_type : str
      Color type
    colors_gl : (count,) list
      Colors to pass to pyglet
    """

    colors = np.asanyarray(colors)
    count = int(count)
    # get the GL kind of color we have
    colors_dtypes = {'f': 'f',
                     'i': 'B',
                     'u': 'B'}

    if colors.dtype.kind in colors_dtypes:
        dtype = colors_dtypes[colors.dtype.kind]
    else:
        dtype = None

    if dtype is not None and util.is_shape(colors, (count, (3, 4))):
        # save the shape and dtype for opengl color string
        colors_type = 'c{}{}/static'.format(colors.shape[1], dtype)
        # reshape the 2D array into a 1D one and then convert to a python list
        gl_colors = colors.reshape(-1).tolist()
    elif dtype is not None and colors.shape in [(3,), (4,)]:
        # we've been passed a single color so tile them
        gl_colors = (np.ones((count, colors.size),
                             dtype=colors.dtype) * colors).reshape(-1).tolist()
        # we know we're tiling
        colors_type = 'c{}{}/static'.format(colors.size, dtype)
    else:
        # case where colors are wrong shape
        # use black as the default color
        gl_colors = np.tile([0.0, 0.0, 0.0],
                            (count, 1)).reshape(-1).tolist()
        # we're returning RGB float colors
        colors_type = 'c3f/static'

    return colors_type, gl_colors


def material_to_texture(material, upsize=True):
    """
    Convert a trimesh.visual.texture.Material object into
    a pyglet-compatible texture object.

    Parameters
    --------------
    material : trimesh.visual.texture.Material
      Material to be converted
    upsize: bool
      If True, will upscale textures to their nearest power
      of two resolution to avoid weirdness

    Returns
    ---------------
    texture : pyglet.image.Texture
      Texture loaded into pyglet form
    """
    import pyglet

    # try to extract a PIL image from material
    if hasattr(material, 'image'):
        img = material.image
    elif hasattr(material, 'baseColorTexture'):
        img = material.baseColorTexture
    else:
        return None

    # if no images in texture return now
    if img is None:
        return None

    # if we're not powers of two upsize
    if upsize:
        from .visual.texture import power_resize
        img = power_resize(img)

    # use a PNG export to exchange into pyglet
    # probably a way to do this with a PIL converter
    with util.BytesIO() as f:
        # export PIL image as PNG
        img.save(f, format='png')
        f.seek(0)
        # filename used for format guess
        gl_image = pyglet.image.load(filename='.png', file=f)

    # turn image into pyglet texture
    texture = gl_image.get_texture()

    return texture


def matrix_to_gl(matrix):
    """
    Convert a numpy row-major homogeneous transformation matrix
    to a flat column-major GLfloat transformation.

    Parameters
    -------------
    matrix : (4,4) float
      Row-major homogeneous transform

    Returns
    -------------
    glmatrix : (16,) gl.GLfloat
      Transform in pyglet format
    """
    from pyglet import gl

    # convert to GLfloat, switch to column major and flatten to (16,)
    return (gl.GLfloat * 16)(*np.array(
        matrix, dtype=np.float32).T.ravel())


def vector_to_gl(array, *args):
    """
    Convert an array and an optional set of args into a
    flat vector of gl.GLfloat
    """
    from pyglet import gl

    array = np.array(array)
    if len(args) > 0:
        array = np.append(array, args)
    vector = (gl.GLfloat * len(array))(*array)
    return vector


def light_to_gl(light, transform, lightN):
    """
    Convert trimesh.scene.lighting.Light objects into
    args for gl.glLightFv calls

    Parameters
    --------------
    light : trimesh.scene.lighting.Light
      Light object to be converted to GL
    transform : (4, 4) float
      Transformation matrix of light
    lightN : int
      Result of gl.GL_LIGHT0, gl.GL_LIGHT1, etc

    Returns
    --------------
    multiarg : [tuple]
      List of args to pass to gl.glLightFv eg:
      [gl.glLightfb(*a) for a in multiarg]
    """
    from pyglet import gl

    # convert color to opengl
    gl_color = vector_to_gl(light.color.astype(np.float64) / 255.0)
    assert len(gl_color) == 4

    # cartesian translation from matrix
    gl_position = vector_to_gl(transform[:3, 3])

    # create the different position and color arguments
    args = [(lightN, gl.GL_POSITION, gl_position),
            (lightN, gl.GL_SPECULAR, gl_color),
            (lightN, gl.GL_DIFFUSE, gl_color),
            (lightN, gl.GL_AMBIENT, gl_color)]
    return args


# trimesh/repair.py

"""
repair.py
-------------

Fill holes and fix winding and normals of meshes.
"""

import numpy as np

from . import graph
from . import triangles

from .constants import log
from .grouping import group_rows
from .geometry import faces_to_edges


try:
    import networkx as nx
except BaseException as E:
    # create a dummy module which will raise the ImportError
    # or other exception only when someone tries to use networkx
    from .exceptions import ExceptionWrapper
    nx = ExceptionWrapper(E)

try:
    from .path.exchange.misc import faces_to_path
except BaseException as E:
    from .exceptions import ExceptionWrapper
    faces_to_path = ExceptionWrapper(E)


def fix_winding(mesh):
    """
    Traverse and change mesh faces in-place to make sure
    winding is correct with edges on adjacent faces in
    opposite directions.

    Parameters
    -------------
    mesh : Trimesh
      Source geometry to alter in-place.
    """
    # anything we would fix is already done
    if mesh.is_winding_consistent:
        return

    graph_all = nx.from_edgelist(mesh.face_adjacency)
    flipped = 0

    faces = mesh.faces.view(np.ndarray).copy()

    # we are going to traverse the graph using BFS
    # start a traversal for every connected component
    for components in nx.connected_components(graph_all):
        # get a subgraph for this component
        g = graph_all.subgraph(components)
        # get the first node in the graph in a way that works on nx's
        # new API and their old API
        start = next(iter(g.nodes()))

        # we traverse every pair of faces in the graph
        # we modify mesh.faces and mesh.face_normals in place
        for face_pair in nx.bfs_edges(g, start):
            # for each pair of faces, we convert them into edges,
            # find the edge that both faces share and then see if edges
            # are reversed in order as you would expect
            # (2, ) int
            face_pair = np.ravel(face_pair)
            # (2, 3) int
            pair = faces[face_pair]
            # (6, 2) int
            edges = faces_to_edges(pair)
            overlap = group_rows(np.sort(edges, axis=1),
                                 require_count=2)
            if len(overlap) == 0:
                # only happens on non-watertight meshes
                continue
            edge_pair = edges[overlap[0]]
            if edge_pair[0][0] == edge_pair[1][0]:
                # if the edges aren't reversed, invert the order of one face
                flipped += 1
                faces[face_pair[1]] = faces[face_pair[1]][::-1]

    if flipped > 0:
        mesh.faces = faces

    log.debug('flipped %d/%d edges', flipped, len(mesh.faces) * 3)


def fix_inversion(mesh, multibody=False):
    """
    Check to see if a mesh has normals pointing "out."

    Parameters
    -------------
    mesh : trimesh.Trimesh
      Mesh to fix in-place.
    multibody : bool
      If True will try to fix normals on every body
    """
    if multibody:
        groups = graph.connected_components(
            mesh.face_adjacency)
        # escape early for single body
        if len(groups) == 1:
            if mesh.volume < 0.0:
                mesh.invert()
            return
        # mask of faces to flip
        flip = np.zeros(len(mesh.faces), dtype=bool)
        # save these to avoid thrashing cache
        tri = mesh.triangles
        cross = mesh.triangles_cross
        # indexes of mesh.faces, not actual faces
        for faces in groups:
            # calculate the volume of the submesh faces
            volume = triangles.mass_properties(
                tri[faces],
                crosses=cross[faces],
                skip_inertia=True)['volume']
            # if that volume is negative it is either
            # inverted or just total garbage
            if volume < 0.0:
                flip[faces] = True
        # one or more faces needs flipping
        if flip.any():
            # flip normals of necessary faces
            if 'face_normals' in mesh._cache:
                normals = mesh.face_normals.copy()
                normals[flip] *= -1.0
            else:
                normals = None
            # flip faces
            mesh.faces[flip] = np.fliplr(mesh.faces[flip])
            if normals is not None:
                mesh.face_normals = normals

    elif mesh.volume < 0.0:
        mesh.invert()


def fix_normals(mesh, multibody=False):
    """
    Fix the winding and direction of a mesh face and
    face normals in-place.

    Really only meaningful on watertight meshes but will orient all
    faces and winding in a uniform way for non-watertight face
    patches as well.

    Parameters
    -------------
    mesh : trimesh.Trimesh
      Mesh to fix normals on
    multibody : bool
      if True try to correct normals direction
      on every body rather than just one

    Notes
    --------------
    mesh.faces : will flip columns on inverted faces
    """
    # traverse face adjacency to correct winding
    fix_winding(mesh)
    # check to see if a mesh is inverted
    fix_inversion(mesh, multibody=multibody)


def broken_faces(mesh, color=None):
    """
    Return the index of faces in the mesh which break the
    watertight status of the mesh.

    Parameters
    --------------
    mesh : trimesh.Trimesh
      Mesh to check broken faces on
    color: (4,) uint8 or None
      Will set broken faces to this color if not None

    Returns
    ---------------
    broken : (n, ) int
      Indexes of mesh.faces
    """
    adjacency = nx.from_edgelist(mesh.face_adjacency)
    broken = [k for k, v in dict(adjacency.degree()).items()
              if v != 3]
    broken = np.array(broken)
    if color is not None and broken.size != 0:
        # if someone passed a broken color
        color = np.array(color)
        if not (color.shape == (4,) or color.shape == (3,)):
            color = [255, 0, 0, 255]
        mesh.visual.face_colors[broken] = color
    return broken


def fill_holes(mesh):
    """
    Fill single- triangle holes on triangular meshes by adding
    new triangles to fill the holes. New triangles will have
    proper winding and normals, and if face colors exist the color
    of the last face will be assigned to the new triangles.

    Parameters
    ------------
    mesh : trimesh.Trimesh
      Mesh will be repaired in- place
    """

    def hole_to_faces(hole):
        """
        Given a loop of vertex indices  representing a hole
        turn it into triangular faces.
        If unable to do so, return None

        Parameters
        -----------
        hole : (n,) int
          Ordered loop of vertex indices

        Returns
        ---------
        faces : (n, 3) int
          New faces
        vertices : (m, 3) float
          New vertices
        """
        hole = np.asanyarray(hole)
        # the case where the hole is just a single missing triangle
        if len(hole) == 3:
            return [hole], []
        # the hole is a quad, which we fill with two triangles
        if len(hole) == 4:
            face_A = hole[[0, 1, 2]]
            face_B = hole[[2, 3, 0]]
            return [face_A, face_B], []
        return [], []

    if len(mesh.faces) < 3:
        return False

    if mesh.is_watertight:
        return True

    # we know that in a watertight mesh every edge will be included twice
    # thus every edge which appears only once is part of a hole boundary
    boundary_groups = group_rows(
        mesh.edges_sorted, require_count=1)

    # mesh is not watertight and we have too few edges
    # edges to do a repair
    # since we haven't changed anything return False
    if len(boundary_groups) < 3:
        return False

    boundary_edges = mesh.edges[boundary_groups]
    index_as_dict = [{'index': i} for i in boundary_groups]

    # we create a graph of the boundary edges, and find cycles.
    g = nx.from_edgelist(
        np.column_stack((boundary_edges,
                         index_as_dict)))
    new_faces = []
    new_vertex = []
    for hole in nx.cycle_basis(g):
        # convert the hole, which is a polygon of vertex indices
        # to triangles and new vertices
        faces, vertex = hole_to_faces(hole=hole)
        if len(faces) == 0:
            continue
        # remeshing returns new vertices as negative indices, so change those
        # to absolute indices which won't be screwed up by the later appends
        faces = np.array(faces)
        faces[faces < 0] += len(new_vertex) + len(mesh.vertices) + len(vertex)
        new_vertex.extend(vertex)
        new_faces.extend(faces)
    new_faces = np.array(new_faces)
    new_vertex = np.array(new_vertex)

    if len(new_faces) == 0:
        # no new faces have been added, so nothing further to do
        # the mesh is NOT watertight, as boundary groups exist
        # but we didn't add any new faces to fill them in
        return False

    for face_index, face in enumerate(new_faces):
        # we compare the edge from the new face with
        # the boundary edge from the source mesh
        edge_test = face[:2]
        edge_boundary = mesh.edges[g.get_edge_data(*edge_test)['index']]

        # in a well constructed mesh, the winding is such that adjacent triangles
        # have reversed edges to each other. Here we check to make sure the
        # edges are reversed, and if they aren't we simply reverse the face
        reversed = edge_test[0] == edge_boundary[1]
        if not reversed:
            new_faces[face_index] = face[::-1]

    # stack vertices into clean (n, 3) float
    if len(new_vertex) != 0:
        new_vertices = np.vstack((mesh.vertices, new_vertex))
    else:
        new_vertices = mesh.vertices

    # try to save face normals if we can
    if 'face_normals' in mesh._cache.cache:
        cached_normals = mesh._cache.cache['face_normals']
    else:
        cached_normals = None

    # also we can remove any zero are triangles by masking here
    new_normals, valid = triangles.normals(new_vertices[new_faces])
    # all the added faces were broken
    if not valid.any():
        return False

    # this is usually the case where two vertices of a triangle are just
    # over tol.merge apart, but the normal calculation is screwed up
    # these could be fixed by merging the vertices in question here:
    # if not valid.all():
    if mesh.visual.defined and mesh.visual.kind == 'face':
        color = mesh.visual.face_colors
    else:
        color = None

    # apply the new faces and vertices
    mesh.faces = np.vstack((mesh._data['faces'], new_faces[valid]))
    mesh.vertices = new_vertices

    # dump the cache and set id to the new hash
    mesh._cache.verify()

    # save us a normals recompute if we can
    if cached_normals is not None:
        mesh.face_normals = np.vstack((cached_normals,
                                       new_normals))

    # this is usually the case where two vertices of a triangle are just
    # over tol.merge apart, but the normal calculation is screwed up
    # these could be fixed by merging the vertices in question here:
    # if not valid.all():
    if color is not None:
        # if face colors exist, assign the last face color to the new faces
        # note that this is a little cheesey, but it is very inexpensive and
        # is the right thing to do if the mesh is a single color.
        color_shape = np.shape(color)
        if len(color_shape) == 2:
            new_colors = np.tile(color[-1], (np.sum(valid), 1))
            new_colors = np.vstack((color,
                                    new_colors))
            mesh.visual.face_colors = new_colors

    log.debug('Filled in mesh with %i triangles', np.sum(valid))
    return mesh.is_watertight


def stitch(mesh, faces=None, insert_vertices=False):
    """
    Create a fan stitch over the boundary of the specified
    faces. If the boundary is non-convex a triangle fan
    is going to be extremely wonky.

    Parameters
    -----------
    vertices : (n, 3) float
      Vertices in space.
    faces : (n,) int
      Face indexes to stitch with triangle fans.
    insert_vertices : bool
      Allow stitching to insert new vertices?

    Returns
    ----------
    fan : (m, 3) int
      New triangles referencing mesh.vertices.
    vertices : (p, 3) float
      Inserted vertices (only returned `if insert_vertices`)
    """
    if faces is None:
        faces = np.arange(len(mesh.faces))

    # get a sequence of vertex indices representing the
    # boundary of the specified faces
    # will be referencing the same indexes of `mesh.vertices`
    points = [e.points for e in
              faces_to_path(mesh, faces)['entities']
              if len(e.points) > 3 and
              e.points[0] == e.points[-1]]

    # get properties to avoid querying in loop
    vertices = mesh.vertices
    normals = mesh.face_normals

    # find which faces are associated with an edge
    edges_face = mesh.edges_face
    tree_edge = mesh.edges_sorted_tree

    if insert_vertices:
        # create one new vertex per curve at the centroid
        centroids = np.array([vertices[p].mean(axis=0)
                              for p in points])
        # save the original length of the vertices
        count = len(vertices)
        # for the normal check stack our local vertices
        vertices = np.vstack((vertices, centroids))
        # create a triangle between our new centroid vertex
        # and each one of the boundary curves
        fan = [np.column_stack((
            np.ones(len(p) - 1, dtype=int) * (count + i),
            p[:-1],
            p[1:]))
            for i, p in enumerate(points)]
    else:
        # since we're not allowed to insert new vertices
        # create a triangle fan for each boundary curve
        fan = [np.column_stack((
            np.ones(len(p) - 3, dtype=int) * p[0],
            p[1:-2],
            p[2:-1]))
            for p in points]

    # now we do a normal check against an adjacent face
    # to see if each region needs to be flipped
    for i, t in zip(range(len(fan)), fan):
        # get the edges from the original mesh
        # for the first `n` new triangles
        e = t[:10, 1:].copy()
        e.sort(axis=1)

        # find which indexes of `mesh.edges` these
        # new edges correspond with by finding edges
        # that exactly correspond with the tree
        query = tree_edge.query_ball_point(e, r=1e-10)
        if len(query) == 0:
            continue
        # stack all the indices that exist
        edge_index = np.concatenate(query)

        # get the normals from the original mesh
        original = normals[edges_face[edge_index]]

        # calculate the normals for a few new faces
        check, valid = triangles.normals(vertices[t[:3]])
        if not valid.any():
            continue
        # take the first valid normal from our new faces
        check = check[valid][0]

        # if our new faces are reversed from the original
        # Adjacent face flip them along their axis
        sign = np.dot(original, check)
        if sign.mean() < 0:
            fan[i] = np.fliplr(t)

    fan = np.vstack(fan)

    if insert_vertices:
        return fan, centroids
    return fan


# trimesh/resolvers.py

"""
resolvers.py
---------------

Provides a common interface to load assets referenced by name
like MTL files, texture images, etc. Assets can be from ZIP
archives, web assets, or a local file path.
"""

import os
import abc
import itertools

from . import util
from . import caching

# URL parsing for remote resources via WebResolver
try:
    # Python 3
    from urllib.parse import urlparse
except ImportError:
    # Python 2
    from urlparse import urlparse


class Resolver(util.ABC):
    """
    The base class for resolvers.
    """

    @abc.abstractmethod
    def __init__(self, *args, **kwargs):
        raise NotImplementedError('Use a resolver subclass!')

    @abc.abstractmethod
    def get(self, key):
        raise NotImplementedError()

    @abc.abstractmethod
    def write(self, name, data):
        raise NotImplementedError('`write` not implemented!')

    @abc.abstractmethod
    def namespaced(self, namespace):
        raise NotImplementedError('`namespaced` not implemented!')

    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, value):
        return self.write(key, value)

    def __contains__(self, key):
        return key in self.keys()


class FilePathResolver(Resolver):
    """
    Resolve files from a source path on the file system.
    """

    def __init__(self, source):
        """
        Resolve files based on a source path.

        Parameters
        ------------
        source : str
          File path where mesh was loaded from
        """
        # remove everything other than absolute path
        clean = os.path.expanduser(
            os.path.abspath(str(source)))

        self.clean = clean
        if os.path.isdir(clean):
            # if we were passed a directory use it
            self.parent = clean
        else:
            # otherwise get the parent directory we've been passed
            split = os.path.split(clean)
            self.parent = split[0]

        # exit if directory doesn't exist
        if not os.path.isdir(self.parent):
            raise ValueError(
                'path `{} `not a directory!'.format(
                    self.parent))

    def keys(self):
        """
        List all files available to be loaded.

        Yields
        -----------
        name : str
          Name of a file which can be accessed.
        """
        parent = self.parent
        for path, _, names in os.walk(self.parent):
            # strip any leading parent key
            if path.startswith(parent):
                path = path[len(parent):]
            # yield each name
            for name in names:
                yield os.path.join(path, name)

    def namespaced(self, namespace):
        """
        Return a resolver which changes the root of the
        resolver by an added namespace.

        Parameters
        -------------
        namespace : str
          Probably a subdirectory

        Returns
        --------------
        resolver : FilePathResolver
          Resolver with root directory changed.
        """
        return FilePathResolver(os.path.join(
            self.parent, namespace))

    def get(self, name):
        """
        Get an asset.

        Parameters
        -------------
        name : str
          Name of the asset

        Returns
        ------------
        data : bytes
          Loaded data from asset
        """
        # load the file by path name
        path = os.path.join(self.parent, name.strip())
        if not os.path.exists(path):
            path = os.path.join(
                self.parent, os.path.split(name)[-1])
        with open(path, 'rb') as f:
            data = f.read()
        return data

    def write(self, name, data):
        """
        Write an asset to a file path.

        Parameters
        -----------
        name : str
          Name of the file to write
        data : str or bytes
          Data to write to the file
        """
        # write files to path name
        with open(os.path.join(self.parent, name.strip()), 'wb') as f:
            # handle encodings correctly for str/bytes
            util.write_encoded(file_obj=f, stuff=data)


class ZipResolver(Resolver):
    """
    Resolve files inside a ZIP archive.
    """

    def __init__(self, archive=None, namespace=None):
        """
        Resolve files inside a ZIP archive as loaded by
        trimesh.util.decompress

        Parameters
        -------------
        archive : dict
          Contains resources as file object
        namespace : None or str
          If passed will only show keys that start
          with this value and this substring must be
          removed for any get calls.
        """
        self.archive = archive
        if isinstance(namespace, str):
            self.namespace = namespace.strip().rstrip('/') + '/'
        else:
            self.namespace = None

    def keys(self):
        """
        Get the available keys in the current archive.

        Returns
        -----------
        keys : iterable
          Keys in the current archive.
        """
        if self.namespace is not None:
            namespace = self.namespace
            length = len(namespace)
            # only return keys that start with the namespace
            # and strip off the namespace from the returned
            # keys.
            return [k[length:] for k in self.archive.keys()
                    if k.startswith(namespace)
                    and len(k) > length]
        return self.archive.keys()

    def write(self, key, value):
        """
        Store a value in the current archive.

        Parameters
        -----------
        key : hashable
          Key to store data under.
        value : str, bytes, file-like
          Value to store.
        """
        if self.archive is None:
            self.archive = {}
        self.archive[key] = value

    def get(self, name):
        """
        Get an asset from the ZIP archive.

        Parameters
        -------------
        name : str
          Name of the asset

        Returns
        -------------
        data : bytes
          Loaded data from asset
        """
        # not much we can do with None
        if name is None:
            return
        # make sure name is a string
        if hasattr(name, 'decode'):
            name = name.decode('utf-8')
        # store reference to archive inside this function
        archive = self.archive
        # requested name not identical in
        # storage so attempt to recover
        if name not in archive:
            # loop through unique results
            for option in nearby_names(name, self.namespace):
                if option in archive:
                    # cleaned option is in archive
                    # so store value and exit
                    name = option
                    break

        # get the stored data
        obj = archive[name]
        # if the dict is storing data as bytes just return
        if isinstance(obj, (bytes, str)):
            return obj
        # otherwise get it as a file object
        # read file object from beginning
        obj.seek(0)
        # data is stored as a file object
        data = obj.read()
        obj.seek(0)
        return data

    def namespaced(self, namespace):
        """
        Return a "sub-resolver" with a root namespace.

        Parameters
        -------------
        namespace : str
          The root of the key to clip off, i.e. if
          this resolver has key `a/b/c` you can get
          'a/b/c' with resolver.namespaced('a/b').get('c')

        Returns
        -----------
        resolver : Resolver
          Namespaced resolver.
        """
        return ZipResolver(archive=self.archive,
                           namespace=namespace)

    def export(self):
        """
        Export the contents of the current archive as
        a ZIP file.

        Returns
        ------------
        compressed : bytes
          Compressed data in ZIP format.
        """
        return util.compress(self.archive)


class WebResolver(Resolver):
    """
    Resolve assets from a remote URL.
    """

    def __init__(self, url):
        """
        Resolve assets from a base URL.

        Parameters
        --------------
        url : str
          Location where a mesh was stored or
          directory where mesh was stored
        """
        if hasattr(url, 'decode'):
            url = url.decode('utf-8')

        # parse string into namedtuple
        parsed = urlparse(url)

        # we want a base url
        split = [i for i in parsed.path.split('/')
                 if len(i) > 0]

        # if the last item in the url path is a filename
        # move up a "directory" for the base path
        if len(split) == 0:
            path = ''
        elif '.' in split[-1]:
            # clip off last item
            path = '/'.join(split[:-1])
        else:
            # recombine into string ignoring any double slashes
            path = '/'.join(split)
        self.base_url = '/'.join(i for i in [
            parsed.scheme + ':/',
            parsed.netloc.strip('/'),
            path.strip('/')] if len(i) > 0) + '/'

        # our string handling should have never inserted double slashes
        assert '//' not in self.base_url[len(parsed.scheme) + 3:]
        # we should always have ended with a single slash
        assert self.base_url.endswith('/')

    def get(self, name):
        """
        Get a resource from the remote site.

        Parameters
        -------------
        name : str
          Asset name, i.e. 'quadknot.obj.mtl'
        """
        # do import here to keep soft dependency
        import requests

        # remove leading and trailing whitespace
        name = name.strip()
        # fetch the data from the remote url

        # base url has been carefully formatted
        url = self.base_url + name

        response = requests.get(url)

        if response.status_code != 200:
            # try to strip off filesystem crap
            if name.startswith('./'):
                name = name[2:]
            response = requests.get(self.base_url + name)

        if response.status_code == '404':
            raise ValueError(response.content)

        # return the bytes of the response
        return response.content

    def namespaced(self, namespace):
        """
        Return a namespaced version of current resolver.

        Parameters
        -------------
        namespace : str
          URL fragment

        Returns
        -----------
        resolver : WebResolver
          With sub-url: `https://example.com/{namespace}`
        """
        # join the base url and the namespace
        return WebResolver(url=self.base_url + namespace)

    def write(self, key, value):
        raise NotImplementedError("can't write to remote")


class GithubResolver(Resolver):
    def __init__(self,
                 repo,
                 branch=None,
                 commit=None,
                 save=None):
        """
        Get files from a remote Github repository by
        downloading a zip file with the entire branch
        or a specific commit.

        Parameters
        -------------
        repo : str
          In the format of `owner/repo`
        branch : str
          The remote branch you want to get files from.
        commit : str
          The full commit hash: pass either this OR branch.
        save : None or str
          A path if you want to save results locally.
        """
        # the github URL for the latest commit of a branch.
        if commit is None:
            self.url = (
                'https://github.com/{repo}/archive/' +
                'refs/heads/{branch}.zip').format(
                    repo=repo, branch=branch)
        else:
            # get a commit URL
            self.url = ('https://github.com/{repo}/archive/' +
                        '{commit}.zip').format(
                            repo=repo, commit=commit)

        if save is not None:
            self.cache = caching.DiskCache(save)
        else:
            self.cache = None

    def keys(self):
        """
        List the available files in the repository.

        Returns
        ----------
        keys : iterable
          Keys available to the resolved.
        """
        return self.zipped.keys()

    def write(self, name, data):
        raise NotImplementedError('`write` not implemented!')

    @property
    def zipped(self):
        """

        - opened zip file
        - locally saved zip file
        - retrieve zip file and saved
        """
        def fetch():
            """
            Fetch the remote zip file.
            """
            import requests
            response = requests.get(self.url)
            if not response.ok:
                raise ValueError(response.content)
            return response.content

        if hasattr(self, '_zip'):
            return self._zip
        # download the archive or get from disc
        raw = self.cache.get(self.url, fetch)
        # create a zip resolver for the archive
        self._zip = ZipResolver(util.decompress(
            util.wrap_as_stream(raw), file_type='zip'))

        return self._zip

    def get(self, key):
        return self.zipped.get(key)

    def namespaced(self, namespace):
        """
        Return a "sub-resolver" with a root namespace.

        Parameters
        -------------
        namespace : str
          The root of the key to clip off, i.e. if
          this resolver has key `a/b/c` you can get
          'a/b/c' with resolver.namespaced('a/b').get('c')

        Returns
        -----------
        resolver : Resolver
          Namespaced resolver.
        """
        return self.zipped.namespaced(namespace)


def nearby_names(name, namespace=None):
    """
    Try to find nearby variants of a specified name.

    Parameters
    ------------
    name : str
      Initial name.

    Yields
    -----------
    nearby : str
      Name that is a lightly permutated version
      of the initial name.
    """
    # the various operations that *might* result in a correct key
    def trim(prefix, item):
        if item.startswith(prefix):
            return item[len(prefix):]
        return item

    cleaners = [lambda x: x,
                lambda x: x.strip(),
                lambda x: trim('./', x),
                lambda x: trim('.\\', x),
                lambda x: trim('\\', x),
                lambda x: os.path.split(x)[-1],
                lambda x: x.replace('%20', ' ')]

    if namespace is None:
        namespace = ''

    # make sure we don't return repeat values
    hit = set()
    for f in cleaners:
        # try just one cleaning function
        current = f(name)
        if current in hit:
            continue
        hit.add(current)
        yield namespace + current

    for a, b in itertools.combinations(cleaners, 2):
        # apply both clean functions
        current = a(b(name))
        if current in hit:
            continue
        hit.add(current)
        yield namespace + current

        # try applying in reverse order
        current = b(a(name))
        if current in hit:
            continue
        hit.add(current)
        yield namespace + current

    if '..' in name and namespace is not None:
        # if someone specified relative paths give it one attempt
        strip = namespace.strip('/').split('/')[:-name.count('..')]
        strip.extend(name.split('..')[-1].strip('/').split('/'))
        yield '/'.join(strip)


# trimesh/sample.py

"""
sample.py
------------

Randomly sample surface and volume of meshes.
"""

import numpy as np

from . import util
from . import transformations
from .visual import uv_to_interpolated_color

if hasattr(np.random, 'default_rng'):
    # newer versions of Numpy
    default_rng = np.random.default_rng
else:
    # Python 2 Numpy
    class default_rng(np.random.RandomState):
        def random(self, *args, **kwargs):
            return self.random_sample(*args, **kwargs)


def sample_surface(mesh, count, face_weight=None, sample_color=False, seed=None):
    """
    Sample the surface of a mesh, returning the specified
    number of points

    For individual triangle sampling uses this method:
    http://mathworld.wolfram.com/TrianglePointPicking.html

    Parameters
    -----------
    mesh : trimesh.Trimesh
      Geometry to sample the surface of
    count : int
      Number of points to return
    face_weight : None or len(mesh.faces) float
      Weight faces by a factor other than face area.
      If None will be the same as face_weight=mesh.area
    sample_color : bool
      Option to calculate the color of the sampled points.
      Default is False.
    seed : None or int
      If passed as an integer will provide deterministic results
      otherwise pulls the seed from operating system entropy.

    Returns
    ---------
    samples : (count, 3) float
      Points in space on the surface of mesh
    face_index : (count,) int
      Indices of faces for each sampled point
    colors : (count, 4) float
      Colors of each sampled point
      Returns only when the sample_color is True
    """

    if face_weight is None:
        # len(mesh.faces) float, array of the areas
        # of each face of the mesh
        face_weight = mesh.area_faces

    # cumulative sum of weights (len(mesh.faces))
    weight_cum = np.cumsum(face_weight)

    # seed the random number generator as requested
    random = default_rng(seed).random

    # last value of cumulative sum is total summed weight/area
    face_pick = random(count) * weight_cum[-1]
    # get the index of the selected faces
    face_index = np.searchsorted(weight_cum, face_pick)

    # pull triangles into the form of an origin + 2 vectors
    tri_origins = mesh.vertices[mesh.faces[:, 0]]
    tri_vectors = mesh.vertices[mesh.faces[:, 1:]].copy()
    tri_vectors -= np.tile(tri_origins, (1, 2)).reshape((-1, 2, 3))

    # pull the vectors for the faces we are going to sample from
    tri_origins = tri_origins[face_index]
    tri_vectors = tri_vectors[face_index]

    if sample_color and hasattr(mesh.visual, 'uv'):
        uv_origins = mesh.visual.uv[mesh.faces[:, 0]]
        uv_vectors = mesh.visual.uv[mesh.faces[:, 1:]].copy()
        uv_origins_tile = np.tile(uv_origins, (1, 2)).reshape((-1, 2, 2))
        uv_vectors -= uv_origins_tile
        uv_origins = uv_origins[face_index]
        uv_vectors = uv_vectors[face_index]

    # randomly generate two 0-1 scalar components to multiply edge vectors b
    random_lengths = random((len(tri_vectors), 2, 1))

    # points will be distributed on a quadrilateral if we use 2 0-1 samples
    # if the two scalar components sum less than 1.0 the point will be
    # inside the triangle, so we find vectors longer than 1.0 and
    # transform them to be inside the triangle
    random_test = random_lengths.sum(axis=1).reshape(-1) > 1.0
    random_lengths[random_test] -= 1.0
    random_lengths = np.abs(random_lengths)

    # multiply triangle edge vectors by the random lengths and sum
    sample_vector = (tri_vectors * random_lengths).sum(axis=1)

    # finally, offset by the origin to generate
    # (n,3) points in space on the triangle
    samples = sample_vector + tri_origins

    if sample_color:
        if hasattr(mesh.visual, 'uv'):
            sample_uv_vector = (uv_vectors * random_lengths).sum(axis=1)
            uv_samples = sample_uv_vector + uv_origins
            texture = mesh.visual.material.image
            colors = uv_to_interpolated_color(uv_samples, texture)
        else:
            colors = mesh.visual.face_colors[face_index]

        return samples, face_index, colors

    return samples, face_index


def volume_mesh(mesh, count):
    """
    Use rejection sampling to produce points randomly
    distributed in the volume of a mesh.


    Parameters
    -----------
    mesh : trimesh.Trimesh
      Geometry to sample
    count : int
      Number of points to return

    Returns
    ---------
    samples : (n, 3) float
      Points in the volume of the mesh where n <= count
    """
    points = (np.random.random((count, 3)) * mesh.extents) + mesh.bounds[0]
    contained = mesh.contains(points)
    samples = points[contained][:count]
    return samples


def volume_rectangular(extents,
                       count,
                       transform=None):
    """
    Return random samples inside a rectangular volume,
    useful for sampling inside oriented bounding boxes.

    Parameters
    -----------
    extents :   (3,) float
      Side lengths of rectangular solid
    count : int
      Number of points to return
    transform : (4, 4) float
      Homogeneous transformation matrix

    Returns
    ---------
    samples : (count, 3) float
      Points in requested volume
    """
    samples = np.random.random((count, 3)) - .5
    samples *= extents
    if transform is not None:
        samples = transformations.transform_points(samples,
                                                   transform)
    return samples


def sample_surface_even(mesh, count, radius=None, seed=None):
    """
    Sample the surface of a mesh, returning samples which are
    VERY approximately evenly spaced. This is accomplished by
    sampling and then rejecting pairs that are too close together.

    Note that since it is using rejection sampling it may return
    fewer points than requested (i.e. n < count). If this is the
    case a log.warning will be emitted.

    Parameters
    -----------
    mesh : trimesh.Trimesh
      Geometry to sample the surface of
    count : int
      Number of points to return
    radius : None or float
      Removes samples below this radius
    seed : None or int
      Provides deterministic values

    Returns
    ---------
    samples : (n, 3) float
      Points in space on the surface of mesh
    face_index : (n,) int
      Indices of faces for each sampled point
    """
    from .points import remove_close

    # guess radius from area
    if radius is None:
        radius = np.sqrt(mesh.area / (3 * count))

    # get points on the surface
    points, index = sample_surface(mesh, count * 3, seed=seed)

    # remove the points closer than radius
    points, mask = remove_close(points, radius)

    # we got all the samples we expect
    if len(points) >= count:
        return points[:count], index[mask][:count]

    # warn if we didn't get all the samples we expect
    util.log.warning('only got {}/{} samples!'.format(
        len(points), count))

    return points, index[mask]


def sample_surface_sphere(count):
    """
    Correctly pick random points on the surface of a unit sphere

    Uses this method:
    http://mathworld.wolfram.com/SpherePointPicking.html

    Parameters
    -----------
    count : int
      Number of points to return

    Returns
    ----------
    points : (count, 3) float
      Random points on the surface of a unit sphere
    """
    # get random values 0.0-1.0
    u, v = np.random.random((2, count))
    # convert to two angles
    theta = np.pi * 2 * u
    phi = np.arccos((2 * v) - 1)
    # convert spherical coordinates to cartesian
    points = util.spherical_to_vector(
        np.column_stack((theta, phi)))
    return points


# trimesh/schemas.py

"""
schemas.py
-------------

Tools for dealing with schemas, particularly JSONschema
"""
import json

from .util import decode_text


def resolve(item, resolver):
    """
    Given a JSON Schema containing `$ref` keys recursively
    evaluate to find and replace referenced files with their
    actual values using trimesh.resolvers.Resolver objects.

    Parameters
    ---------------
    item : any
      JSON schema including `$ref` to other files
    resolver : trimesh.visual.resolver.Resolver
      Resolver to fetch referenced assets

    Returns
    ----------
    result : any
      JSONSchema with references replaced
    """
    if isinstance(item, list):
        # run the resolver on every list item
        return [resolve(i, resolver) for i in item]
    elif isinstance(item, dict):
        if '$ref' in item:
            # if we have a reference to a file pop the key
            # and update the dict with the reference in-place
            raw = decode_text(resolver.get(item.pop('$ref')))
            item.update(json.loads(raw))
            # run the resolver on the dict again
            resolve(item, resolver)
        else:
            # make sure all keys are evaluated
            for i in item.values():
                resolve(i, resolver)
    return item


# trimesh/smoothing.py

import numpy as np

try:
    from scipy.sparse.linalg import spsolve
    from scipy.sparse import coo_matrix, eye
except ImportError:
    pass

from . import triangles
from .util import unitize
from .geometry import index_sparse
from .triangles import mass_properties


def filter_laplacian(mesh,
                     lamb=0.5,
                     iterations=10,
                     implicit_time_integration=False,
                     volume_constraint=True,
                     laplacian_operator=None):
    """
    Smooth a mesh in-place using laplacian smoothing.
    Articles
    1 - "Improved Laplacian Smoothing of Noisy Surface Meshes"
       J. Vollmer, R. Mencl, and H. Muller
    2 - "Implicit Fairing of Irregular Meshes using Diffusion
       and Curvature Flow". M. Desbrun,  M. Meyer,
       P. Schroder, A.H.B. Caltech
    Parameters
    ------------
    mesh : trimesh.Trimesh
    Mesh to be smoothed in place
    lamb : float
    Diffusion speed constant
    If   0.0, no diffusion
    If > 0.0, diffusion occurs
    implicit_time_integration: boolean
    if False: explicit time integration
        -lamb <= 1.0 - Stability Limit (Article 1)
    if True: implicit time integration
        -lamb no limit (Article 2)
    iterations : int
    Number of passes to run filter
    laplacian_operator : None or scipy.sparse.coo.coo_matrix
    Sparse matrix laplacian operator
    Will be autogenerated if None
    """

    # if the laplacian operator was not passed create it here
    if laplacian_operator is None:
        laplacian_operator = laplacian_calculation(mesh)

    # save initial volume
    if volume_constraint:
        vol_ini = mesh.volume

    # get mesh vertices and faces as vanilla numpy array
    vertices = mesh.vertices.copy().view(np.ndarray)
    faces = mesh.faces.copy().view(np.ndarray)

    # Set matrix for linear system of equations
    if implicit_time_integration:
        dlap = laplacian_operator.shape[0]
        AA = eye(dlap) + lamb * (eye(dlap) - laplacian_operator)

    # Number of passes
    for _index in range(iterations):
        # Classic Explicit Time Integration - Article 1
        if not implicit_time_integration:
            dot = laplacian_operator.dot(vertices) - vertices
            vertices += lamb * dot

        # Implicit Time Integration - Article 2
        else:
            vertices = spsolve(AA, vertices)

        # volume constraint
        if volume_constraint:
            # find the volume with new vertex positions
            vol_new = triangles.mass_properties(
                vertices[faces], skip_inertia=True)["volume"]
            # scale by volume ratio
            vertices *= ((vol_ini / vol_new) ** (1.0 / 3.0))

    # assign modified vertices back to mesh
    mesh.vertices = vertices
    return mesh


def filter_humphrey(mesh,
                    alpha=0.1,
                    beta=0.5,
                    iterations=10,
                    laplacian_operator=None):
    """
    Smooth a mesh in-place using laplacian smoothing
    and Humphrey filtering.
    Articles
    "Improved Laplacian Smoothing of Noisy Surface Meshes"
    J. Vollmer, R. Mencl, and H. Muller
    Parameters
    ------------
    mesh : trimesh.Trimesh
      Mesh to be smoothed in place
    alpha : float
      Controls shrinkage, range is 0.0 - 1.0
      If 0.0, not considered
      If 1.0, no smoothing
    beta : float
      Controls how aggressive smoothing is
      If 0.0, no smoothing
      If 1.0, full aggressiveness
    iterations : int
      Number of passes to run filter
    laplacian_operator : None or scipy.sparse.coo.coo_matrix
      Sparse matrix laplacian operator
      Will be autogenerated if None
    """
    # if the laplacian operator was not passed create it here
    if laplacian_operator is None:
        laplacian_operator = laplacian_calculation(mesh)

    # get mesh vertices as vanilla numpy array
    vertices = mesh.vertices.copy().view(np.ndarray)
    # save original unmodified vertices
    original = vertices.copy()

    # run through iterations of filter
    for _index in range(iterations):
        vert_q = vertices.copy()
        vertices = laplacian_operator.dot(vertices)
        vert_b = vertices - (alpha * original + (1.0 - alpha) * vert_q)
        vertices -= (beta * vert_b + (1.0 - beta) *
                     laplacian_operator.dot(vert_b))

    # assign modified vertices back to mesh
    mesh.vertices = vertices
    return mesh


def filter_taubin(mesh,
                  lamb=0.5,
                  nu=0.5,
                  iterations=10,
                  laplacian_operator=None):
    """
    Smooth a mesh in-place using laplacian smoothing
    and taubin filtering.
    Articles
    "Improved Laplacian Smoothing of Noisy Surface Meshes"
    J. Vollmer, R. Mencl, and H. Muller
    Parameters
    ------------
    mesh : trimesh.Trimesh
      Mesh to be smoothed in place.
    lamb : float
      Controls shrinkage, range is 0.0 - 1.0
    nu : float
      Controls dilation, range is 0.0 - 1.0
      Nu shall be between 0.0 < 1.0/lambda - 1.0/nu < 0.1
    iterations : int
      Number of passes to run the filter
    laplacian_operator : None or scipy.sparse.coo.coo_matrix
      Sparse matrix laplacian operator
      Will be autogenerated if None
    """
    # if the laplacian operator was not passed create it here
    if laplacian_operator is None:
        laplacian_operator = laplacian_calculation(mesh)

    # get mesh vertices as vanilla numpy array
    vertices = mesh.vertices.copy().view(np.ndarray)

    # run through multiple passes of the filter
    for index in range(iterations):
        # do a sparse dot product on the vertices
        dot = laplacian_operator.dot(vertices) - vertices
        # alternate shrinkage and dilation
        if index % 2 == 0:
            vertices += lamb * dot
        else:
            vertices -= nu * dot

    # assign updated vertices back to mesh
    mesh.vertices = vertices
    return mesh


def filter_mut_dif_laplacian(mesh,
                             lamb=0.5,
                             iterations=10,
                             volume_constraint=True,
                             laplacian_operator=None):
    """
    Smooth a mesh in-place using laplacian smoothing using a
    mutable difusion laplacian.

    Articles
      Barroqueiro, B., Andrade-Campos, A., Dias-de-Oliveira,
      J., and Valente, R. (January 21, 2021).
      "Bridging between topology optimization and additive
       manufacturing via Laplacian smoothing." ASME. J. Mech. Des.


    Parameters
    ------------
    mesh : trimesh.Trimesh
      Mesh to be smoothed in place
    lamb : float
      Diffusion speed constant
      If   0.0, no diffusion
      If > 0.0, diffusion occurs
    iterations : int
      Number of passes to run filter
    laplacian_operator : None or scipy.sparse.coo.coo_matrix
      Sparse matrix laplacian operator
      Will be autogenerated if None
    """

    # if the laplacian operator was not passed create it here
    if laplacian_operator is None:
        laplacian_operator = laplacian_calculation(mesh)

    # Set volume constraint
    if volume_constraint:
        v_ini = mesh.volume

    # get mesh vertices as vanilla numpy array
    vertices = mesh.vertices.copy().view(np.ndarray)
    faces = mesh.faces.copy().view(np.ndarray)
    eps = 0.01 * (np.max(mesh.area_faces)**0.5)

    # Number of passes
    for _index in range(iterations):

        # Mutable difusion
        normals = get_vertices_normals(mesh)
        qi = laplacian_operator.dot(vertices)
        pi_qi = vertices - qi
        adil = np.abs((normals * pi_qi).dot(np.ones((3, 1))))
        adil = 1.0 / np.maximum(1e-12, adil)
        lamber = np.maximum(
            0.2 * lamb, np.minimum(1.0, lamb * adil / np.mean(adil)))

        # Filter
        dot = laplacian_operator.dot(vertices) - vertices
        vertices += lamber * dot

        # Volume constraint
        if volume_constraint:
            vol = mass_properties(vertices[faces], skip_inertia=True)["volume"]
            if _index == 0:
                slope = dilate_slope(vertices, faces, normals, vol, eps)
            vertices += normals * slope * (v_ini - vol)

    # assign modified vertices back to mesh
    mesh.vertices = vertices

    return mesh


def laplacian_calculation(mesh, equal_weight=True, pinned_vertices=None):
    """
    Calculate a sparse matrix for laplacian operations.
    Parameters
    -------------
    mesh : trimesh.Trimesh
      Input geometry
    equal_weight : bool
      If True, all neighbors will be considered equally
      If False, all neighbors will be weighted by inverse distance
    Returns
    ----------
    laplacian : scipy.sparse.coo.coo_matrix
      Laplacian operator
    """
    # get the vertex neighbors from the cache
    neighbors = mesh.vertex_neighbors

    # if a node is pinned, it will average his coordinates by himself
    # in practice it will not move
    if pinned_vertices is not None:
        for i in pinned_vertices:
            neighbors[i] = [i]

    # avoid hitting crc checks in loops
    vertices = mesh.vertices.view(np.ndarray)

    # stack neighbors to 1D arrays
    col = np.concatenate(neighbors)
    row = np.concatenate([[i] * len(n)
                          for i, n in enumerate(neighbors)])

    if equal_weight:
        # equal weights for each neighbor
        data = np.concatenate([[1.0 / len(n)] * len(n)
                               for n in neighbors])
    else:
        # umbrella weights, distance-weighted
        # use dot product of ones to replace array.sum(axis=1)
        ones = np.ones(3)
        # the distance from verticesex to neighbors
        norms = [
            1.0 / np.maximum(1e-6, np.sqrt(np.dot(
                (vertices[i] - vertices[n]) ** 2, ones)))
            for i, n in enumerate(neighbors)]
        # normalize group and stack into single array
        data = np.concatenate([i / i.sum() for i in norms])

    # create the sparse matrix
    matrix = coo_matrix((data, (row, col)),
                        shape=[len(vertices)] * 2)

    return matrix


def get_vertices_normals(mesh):
    """
    Compute Vertex normals using equal weighting of neighbors faces.
    Parameters
      -------------
      mesh : trimesh.Trimesh
        Input geometry
      Returns
      ----------
      vertices_normals: array
        Vertices normals
    """

    # get mesh vertices and faces
    vertices = mesh.vertices
    faces = mesh.faces

    # get face normals
    face_normals = mesh.face_normals

    # Compute Vert normals
    vert_normals = index_sparse(len(vertices), faces).dot(face_normals)

    return unitize(vert_normals)


def dilate_slope(vertices, faces, normals, v, eps):
    """
    Get de derivate of dilation scalar by the volume variation by finite differences
    Thus, Vertices += vertex_normals*dilate_slope*(Initial_Volume - Srinked_Volume)
    Parameters
      -------------
      mesh : trimesh.Trimesh
        Input geometry
      vertices: mesh.vertices
      faces: mesh.faces
      normals: array
        vertices normals
      Returns
      ----------
      dilate_slope: float
        derivative
    """

    # finite difference derivative
    vertices2 = vertices + normals * eps
    v2 = mass_properties(vertices2[faces], skip_inertia=True)["volume"]

    return (eps) / (v2 - v)


# trimesh/transformations.py

# -*- coding: utf-8 -*-
# transformations.py

# Modified for inclusion in the `trimesh` library
# https://github.com/mikedh/trimesh
# -----------------------------------------------------------------------
#
# Copyright (c) 2006-2017, Christoph Gohlke
# Copyright (c) 2006-2017, The Regents of the University of California
# Produced at the Laboratory for Fluorescence Dynamics
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of any
#   contributors may be used to endorse or promote products derived
#   from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""Homogeneous Transformation Matrices and Quaternions.

A library for calculating 4x4 matrices for translating, rotating, reflecting,
scaling, shearing, projecting, orthogonalizing, and superimposing arrays of
3D homogeneous coordinates as well as for converting between rotation matrices,
Euler angles, and quaternions. Also includes an Arcball control object and
functions to decompose transformation matrices.

:Author:
  `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`_

:Organization:
  Laboratory for Fluorescence Dynamics, University of California, Irvine

:Version: 2017.02.17

Requirements
------------
* `CPython 2.7 or 3.4 <http://www.python.org>`_
* `numpy 1.9 <http://www.np.org>`_
* `Transformations.c 2015.03.19 <http://www.lfd.uci.edu/~gohlke/>`_
  (recommended for speedup of some functions)

Notes
-----
The API is not stable yet and is expected to change between revisions.

This Python code is not optimized for speed. Refer to the transformations.c
module for a faster implementation of some functions.

Documentation in HTML format can be generated with epydoc.

Matrices (M) can be inverted using np.linalg.inv(M), be concatenated using
np.dot(M0, M1), or transform homogeneous coordinate arrays (v) using
np.dot(M, v) for shape (4, *) column vectors, respectively
np.dot(v, M.T) for shape (*, 4) row vectors ("array of points").

This module follows the "column vectors on the right" and "row major storage"
(C contiguous) conventions. The translation components are in the right column
of the transformation matrix, i.e. M[:3, 3].
The transpose of the transformation matrices may have to be used to interface
with other graphics systems, e.g. with OpenGL's glMultMatrixd(). See also [16].

Calculations are carried out with np.float64 precision.

Vector, point, quaternion, and matrix function arguments are expected to be
"array like", i.e. tuple, list, or numpy arrays.

Return types are numpy arrays unless specified otherwise.

Angles are in radians unless specified otherwise.

Quaternions w+ix+jy+kz are represented as [w, x, y, z].

A triple of Euler angles can be applied/interpreted in 24 ways, which can
be specified using a 4 character string or encoded 4-tuple:

  *Axes 4-string*: e.g. 'sxyz' or 'ryxy'

  - first character : rotations are applied to 's'tatic or 'r'otating frame
  - remaining characters : successive rotation axis 'x', 'y', or 'z'

  *Axes 4-tuple*: e.g. (0, 0, 0, 0) or (1, 1, 1, 1)

  - inner axis: code of axis ('x':0, 'y':1, 'z':2) of rightmost matrix.
  - parity : even (0) if inner axis 'x' is followed by 'y', 'y' is followed
    by 'z', or 'z' is followed by 'x'. Otherwise odd (1).
  - repetition : first and last axis are same (1) or different (0).
  - frame : rotations are applied to static (0) or rotating (1) frame.

Other Python packages and modules for 3D transformations and quaternions:

* `Transforms3d <https://pypi.python.org/pypi/transforms3d>`_
   includes most code of this module.
* `Blender.mathutils <http://www.blender.org/api/blender_python_api>`_
* `numpy-dtypes <https://github.com/numpy/numpy-dtypes>`_

References
----------
(1)  Matrices and transformations. Ronald Goldman.
     In "Graphics Gems I", pp 472-475. Morgan Kaufmann, 1990.
(2)  More matrices and transformations: shear and pseudo-perspective.
     Ronald Goldman. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
(3)  Decomposing a matrix into simple transformations. Spencer Thomas.
     In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
(4)  Recovering the data from the transformation matrix. Ronald Goldman.
     In "Graphics Gems II", pp 324-331. Morgan Kaufmann, 1991.
(5)  Euler angle conversion. Ken Shoemake.
     In "Graphics Gems IV", pp 222-229. Morgan Kaufmann, 1994.
(6)  Arcball rotation control. Ken Shoemake.
     In "Graphics Gems IV", pp 175-192. Morgan Kaufmann, 1994.
(7)  Representing attitude: Euler angles, unit quaternions, and rotation
     vectors. James Diebel. 2006.
(8)  A discussion of the solution for the best rotation to relate two sets
     of vectors. W Kabsch. Acta Cryst. 1978. A34, 827-828.
(9)  Closed-form solution of absolute orientation using unit quaternions.
     BKP Horn. J Opt Soc Am A. 1987. 4(4):629-642.
(10) Quaternions. Ken Shoemake.
     http://www.sfu.ca/~jwa3/cmpt461/files/quatut.pdf
(11) From quaternion to matrix and back. JMP van Waveren. 2005.
     http://www.intel.com/cd/ids/developer/asmo-na/eng/293748.htm
(12) Uniform random rotations. Ken Shoemake.
     In "Graphics Gems III", pp 124-132. Morgan Kaufmann, 1992.
(13) Quaternion in molecular modeling. CFF Karney.
     J Mol Graph Mod, 25(5):595-604
(14) New method for extracting the quaternion from a rotation matrix.
     Itzhack Y Bar-Itzhack, J Guid Contr Dynam. 2000. 23(6): 1085-1087.
(15) Multiple View Geometry in Computer Vision. Hartley and Zissermann.
     Cambridge University Press; 2nd Ed. 2004. Chapter 4, Algorithm 4.7, p 130.
(16) Column Vectors vs. Row Vectors.
     http://steve.hollasch.net/cgindex/math/matrix/column-vec.html

Examples
--------
>>> alpha, beta, gamma = 0.123, -1.234, 2.345
>>> origin, xaxis, yaxis, zaxis = [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]
>>> I = identity_matrix()
>>> Rx = rotation_matrix(alpha, xaxis)
>>> Ry = rotation_matrix(beta, yaxis)
>>> Rz = rotation_matrix(gamma, zaxis)
>>> R = concatenate_matrices(Rx, Ry, Rz)
>>> euler = euler_from_matrix(R, 'rxyz')
>>> np.allclose([alpha, beta, gamma], euler)
True
>>> Re = euler_matrix(alpha, beta, gamma, 'rxyz')
>>> is_same_transform(R, Re)
True
>>> al, be, ga = euler_from_matrix(Re, 'rxyz')
>>> is_same_transform(Re, euler_matrix(al, be, ga, 'rxyz'))
True
>>> qx = quaternion_about_axis(alpha, xaxis)
>>> qy = quaternion_about_axis(beta, yaxis)
>>> qz = quaternion_about_axis(gamma, zaxis)
>>> q = quaternion_multiply(qx, qy)
>>> q = quaternion_multiply(q, qz)
>>> Rq = quaternion_matrix(q)
>>> is_same_transform(R, Rq)
True
>>> S = scale_matrix(1.23, origin)
>>> T = translation_matrix([1, 2, 3])
>>> Z = shear_matrix(beta, xaxis, origin, zaxis)
>>> R = random_rotation_matrix(np.random.rand(3))
>>> M = concatenate_matrices(T, R, Z, S)
>>> scale, shear, angles, trans, persp = decompose_matrix(M)
>>> np.allclose(scale, 1.23)
True
>>> np.allclose(trans, [1, 2, 3])
True
>>> np.allclose(shear, [0, math.tan(beta), 0])
True
>>> is_same_transform(R, euler_matrix(axes='sxyz', *angles))
True
>>> M1 = compose_matrix(scale, shear, angles, trans, persp)
>>> is_same_transform(M, M1)
True
>>> v0, v1 = random_vector(3), random_vector(3)
>>> M = rotation_matrix(angle_between_vectors(v0, v1), vector_product(v0, v1))
>>> v2 = np.dot(v0, M[:3,:3].T)
>>> np.allclose(unit_vector(v1), unit_vector(v2))
True

"""
from __future__ import division, print_function


import math
import numpy as np

__version__ = '2017.02.17'
__docformat__ = 'restructuredtext en'
__all__ = ()

_IDENTITY = np.eye(4)
_IDENTITY.flags['WRITEABLE'] = False


def identity_matrix():
    """Return 4x4 identity/unit matrix.

    >>> I = identity_matrix()
    >>> np.allclose(I, np.dot(I, I))
    True
    >>> np.sum(I), np.trace(I)
    (4.0, 4.0)
    >>> np.allclose(I, np.identity(4))
    True

    """
    return np.identity(4)


def translation_matrix(direction):
    """
    Return matrix to translate by direction vector.

    >>> v = np.random.random(3) - 0.5
    >>> np.allclose(v, translation_matrix(v)[:3, 3])
    True

    """
    # are we 2D or 3D
    dim = len(direction)
    # start with identity matrix
    M = np.identity(dim + 1)
    # apply the offset
    M[:dim, dim] = direction[:dim]
    return M


def translation_from_matrix(matrix):
    """Return translation vector from translation matrix.

    >>> v0 = np.random.random(3) - 0.5
    >>> v1 = translation_from_matrix(translation_matrix(v0))
    >>> np.allclose(v0, v1)
    True

    """
    return np.array(matrix, copy=False)[:3, 3].copy()


def reflection_matrix(point, normal):
    """Return matrix to mirror at plane defined by point and normal vector.

    >>> v0 = np.random.random(4) - 0.5
    >>> v0[3] = 1.
    >>> v1 = np.random.random(3) - 0.5
    >>> R = reflection_matrix(v0, v1)
    >>> np.allclose(2, np.trace(R))
    True
    >>> np.allclose(v0, np.dot(R, v0))
    True
    >>> v2 = v0.copy()
    >>> v2[:3] += v1
    >>> v3 = v0.copy()
    >>> v2[:3] -= v1
    >>> np.allclose(v2, np.dot(R, v3))
    True

    """
    normal = unit_vector(normal[:3])
    M = np.identity(4)
    M[:3, :3] -= 2.0 * np.outer(normal, normal)
    M[:3, 3] = (2.0 * np.dot(point[:3], normal)) * normal
    return M


def reflection_from_matrix(matrix):
    """Return mirror plane point and normal vector from reflection matrix.

    >>> v0 = np.random.random(3) - 0.5
    >>> v1 = np.random.random(3) - 0.5
    >>> M0 = reflection_matrix(v0, v1)
    >>> point, normal = reflection_from_matrix(M0)
    >>> M1 = reflection_matrix(point, normal)
    >>> is_same_transform(M0, M1)
    True

    """
    M = np.array(matrix, dtype=np.float64, copy=False)
    # normal: unit eigenvector corresponding to eigenvalue -1
    w, V = np.linalg.eig(M[:3, :3])
    i = np.where(abs(np.real(w) + 1.0) < 1e-8)[0]
    if not len(i):
        raise ValueError("no unit eigenvector corresponding to eigenvalue -1")
    normal = np.real(V[:, i[0]]).squeeze()
    # point: any unit eigenvector corresponding to eigenvalue 1
    w, V = np.linalg.eig(M)
    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
    if not len(i):
        raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
    point = np.real(V[:, i[-1]]).squeeze()
    point /= point[3]
    return point, normal


def rotation_matrix(angle, direction, point=None):
    """
    Return matrix to rotate about axis defined by point and
    direction.

    Parameters
    -------------
    angle     : float, or sympy.Symbol
      Angle, in radians or symbolic angle
    direction : (3,) float
      Unit vector along rotation axis
    point     : (3, ) float, or None
      Origin point of rotation axis

    Returns
    -------------
    matrix : (4, 4) float, or (4, 4) sympy.Matrix
      Homogeneous transformation matrix

    Examples
    -------------
    >>> R = rotation_matrix(math.pi/2, [0, 0, 1], [1, 0, 0])
    >>> np.allclose(np.dot(R, [0, 0, 0, 1]), [1, -1, 0, 1])
    True
    >>> angle = (random.random() - 0.5) * (2*math.pi)
    >>> direc = np.random.random(3) - 0.5
    >>> point = np.random.random(3) - 0.5
    >>> R0 = rotation_matrix(angle, direc, point)
    >>> R1 = rotation_matrix(angle-2*math.pi, direc, point)
    >>> is_same_transform(R0, R1)
    True
    >>> R0 = rotation_matrix(angle, direc, point)
    >>> R1 = rotation_matrix(-angle, -direc, point)
    >>> is_same_transform(R0, R1)
    True
    >>> I = np.identity(4, np.float64)
    >>> np.allclose(I, rotation_matrix(math.pi*2, direc))
    True
    >>> np.allclose(2, np.trace(rotation_matrix(math.pi/2,direc,point)))
    True

    """
    if type(angle).__name__ == 'Symbol':
        # special case sympy symbolic angles
        import sympy as sp
        symbolic = True
        sina = sp.sin(angle)
        cosa = sp.cos(angle)
    else:
        symbolic = False
        sina = math.sin(angle)
        cosa = math.cos(angle)

    direction = unit_vector(direction[:3])
    # rotation matrix around unit vector
    M = np.diag([cosa, cosa, cosa, 1.0])
    M[:3, :3] += np.outer(direction, direction) * (1.0 - cosa)

    direction = direction * sina
    M[:3, :3] += np.array([[0.0, -direction[2], direction[1]],
                           [direction[2], 0.0, -direction[0]],
                           [-direction[1], direction[0], 0.0]])

    # if point is specified, rotation is not around origin
    if point is not None:
        point = np.array(point[:3], dtype=np.float64, copy=False)
        M[:3, 3] = point - np.dot(M[:3, :3], point)

    # return symbolic angles as sympy Matrix objects
    if symbolic:
        return sp.Matrix(M)

    return M


def rotation_from_matrix(matrix):
    """Return rotation angle and axis from rotation matrix.

    >>> angle = (random.random() - 0.5) * (2*math.pi)
    >>> direc = np.random.random(3) - 0.5
    >>> point = np.random.random(3) - 0.5
    >>> R0 = rotation_matrix(angle, direc, point)
    >>> angle, direc, point = rotation_from_matrix(R0)
    >>> R1 = rotation_matrix(angle, direc, point)
    >>> is_same_transform(R0, R1)
    True

    """
    R = np.array(matrix, dtype=np.float64, copy=False)
    R33 = R[:3, :3]
    # direction: unit eigenvector of R33 corresponding to eigenvalue of 1
    w, W = np.linalg.eig(R33.T)
    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
    if not len(i):
        raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
    direction = np.real(W[:, i[-1]]).squeeze()
    # point: unit eigenvector of R33 corresponding to eigenvalue of 1
    w, Q = np.linalg.eig(R)
    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
    if not len(i):
        raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
    point = np.real(Q[:, i[-1]]).squeeze()
    point /= point[3]
    # rotation angle depending on direction
    cosa = (np.trace(R33) - 1.0) / 2.0
    if abs(direction[2]) > 1e-8:
        sina = (R[1, 0] + (cosa - 1.0) * direction[0]
                * direction[1]) / direction[2]
    elif abs(direction[1]) > 1e-8:
        sina = (R[0, 2] + (cosa - 1.0) * direction[0]
                * direction[2]) / direction[1]
    else:
        sina = (R[2, 1] + (cosa - 1.0) * direction[1]
                * direction[2]) / direction[0]
    angle = math.atan2(sina, cosa)
    return angle, direction, point


def scale_matrix(factor, origin=None, direction=None):
    """Return matrix to scale by factor around origin in direction.

    Use factor -1 for point symmetry.

    >>> v = (np.random.rand(4, 5) - 0.5) * 20
    >>> v[3] = 1
    >>> S = scale_matrix(-1.234)
    >>> np.allclose(np.dot(S, v)[:3], -1.234*v[:3])
    True
    >>> factor = random.random() * 10 - 5
    >>> origin = np.random.random(3) - 0.5
    >>> direct = np.random.random(3) - 0.5
    >>> S = scale_matrix(factor, origin)
    >>> S = scale_matrix(factor, origin, direct)

    """
    if direction is None:
        # uniform scaling
        M = np.diag([factor, factor, factor, 1.0])
        if origin is not None:
            M[:3, 3] = origin[:3]
            M[:3, 3] *= 1.0 - factor
    else:
        # nonuniform scaling
        direction = unit_vector(direction[:3])
        factor = 1.0 - factor
        M = np.identity(4)
        M[:3, :3] -= factor * np.outer(direction, direction)
        if origin is not None:
            M[:3, 3] = (factor * np.dot(origin[:3], direction)) * direction
    return M


def scale_from_matrix(matrix):
    """Return scaling factor, origin and direction from scaling matrix.

    >>> factor = random.random() * 10 - 5
    >>> origin = np.random.random(3) - 0.5
    >>> direct = np.random.random(3) - 0.5
    >>> S0 = scale_matrix(factor, origin)
    >>> factor, origin, direction = scale_from_matrix(S0)
    >>> S1 = scale_matrix(factor, origin, direction)
    >>> is_same_transform(S0, S1)
    True
    >>> S0 = scale_matrix(factor, origin, direct)
    >>> factor, origin, direction = scale_from_matrix(S0)
    >>> S1 = scale_matrix(factor, origin, direction)
    >>> is_same_transform(S0, S1)
    True

    """
    M = np.array(matrix, dtype=np.float64, copy=False)
    M33 = M[:3, :3]
    factor = np.trace(M33) - 2.0
    try:
        # direction: unit eigenvector corresponding to eigenvalue factor
        w, V = np.linalg.eig(M33)
        i = np.where(abs(np.real(w) - factor) < 1e-8)[0][0]
        direction = np.real(V[:, i]).squeeze()
        direction /= vector_norm(direction)
    except IndexError:
        # uniform scaling
        factor = (factor + 2.0) / 3.0
        direction = None
    # origin: any eigenvector corresponding to eigenvalue 1
    w, V = np.linalg.eig(M)
    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
    if not len(i):
        raise ValueError("no eigenvector corresponding to eigenvalue 1")
    origin = np.real(V[:, i[-1]]).squeeze()
    origin /= origin[3]
    return factor, origin, direction


def projection_matrix(point, normal, direction=None,
                      perspective=None, pseudo=False):
    """Return matrix to project onto plane defined by point and normal.

    Using either perspective point, projection direction, or none of both.

    If pseudo is True, perspective projections will preserve relative depth
    such that Perspective = dot(Orthogonal, PseudoPerspective).

    >>> P = projection_matrix([0, 0, 0], [1, 0, 0])
    >>> np.allclose(P[1:, 1:], np.identity(4)[1:, 1:])
    True
    >>> point = np.random.random(3) - 0.5
    >>> normal = np.random.random(3) - 0.5
    >>> direct = np.random.random(3) - 0.5
    >>> persp = np.random.random(3) - 0.5
    >>> P0 = projection_matrix(point, normal)
    >>> P1 = projection_matrix(point, normal, direction=direct)
    >>> P2 = projection_matrix(point, normal, perspective=persp)
    >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True)
    >>> is_same_transform(P2, np.dot(P0, P3))
    True
    >>> P = projection_matrix([3, 0, 0], [1, 1, 0], [1, 0, 0])
    >>> v0 = (np.random.rand(4, 5) - 0.5) * 20
    >>> v0[3] = 1
    >>> v1 = np.dot(P, v0)
    >>> np.allclose(v1[1], v0[1])
    True
    >>> np.allclose(v1[0], 3-v1[1])
    True

    """
    M = np.identity(4)
    point = np.array(point[:3], dtype=np.float64, copy=False)
    normal = unit_vector(normal[:3])
    if perspective is not None:
        # perspective projection
        perspective = np.array(perspective[:3], dtype=np.float64,
                               copy=False)
        M[0, 0] = M[1, 1] = M[2, 2] = np.dot(perspective - point, normal)
        M[:3, :3] -= np.outer(perspective, normal)
        if pseudo:
            # preserve relative depth
            M[:3, :3] -= np.outer(normal, normal)
            M[:3, 3] = np.dot(point, normal) * (perspective + normal)
        else:
            M[:3, 3] = np.dot(point, normal) * perspective
        M[3, :3] = -normal
        M[3, 3] = np.dot(perspective, normal)
    elif direction is not None:
        # parallel projection
        direction = np.array(direction[:3], dtype=np.float64, copy=False)
        scale = np.dot(direction, normal)
        M[:3, :3] -= np.outer(direction, normal) / scale
        M[:3, 3] = direction * (np.dot(point, normal) / scale)
    else:
        # orthogonal projection
        M[:3, :3] -= np.outer(normal, normal)
        M[:3, 3] = np.dot(point, normal) * normal
    return M


def projection_from_matrix(matrix, pseudo=False):
    """Return projection plane and perspective point from projection matrix.

    Return values are same as arguments for projection_matrix function:
    point, normal, direction, perspective, and pseudo.

    >>> point = np.random.random(3) - 0.5
    >>> normal = np.random.random(3) - 0.5
    >>> direct = np.random.random(3) - 0.5
    >>> persp = np.random.random(3) - 0.5
    >>> P0 = projection_matrix(point, normal)
    >>> result = projection_from_matrix(P0)
    >>> P1 = projection_matrix(*result)
    >>> is_same_transform(P0, P1)
    True
    >>> P0 = projection_matrix(point, normal, direct)
    >>> result = projection_from_matrix(P0)
    >>> P1 = projection_matrix(*result)
    >>> is_same_transform(P0, P1)
    True
    >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=False)
    >>> result = projection_from_matrix(P0, pseudo=False)
    >>> P1 = projection_matrix(*result)
    >>> is_same_transform(P0, P1)
    True
    >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=True)
    >>> result = projection_from_matrix(P0, pseudo=True)
    >>> P1 = projection_matrix(*result)
    >>> is_same_transform(P0, P1)
    True

    """
    M = np.array(matrix, dtype=np.float64, copy=False)
    M33 = M[:3, :3]
    w, V = np.linalg.eig(M)
    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
    if not pseudo and len(i):
        # point: any eigenvector corresponding to eigenvalue 1
        point = np.real(V[:, i[-1]]).squeeze()
        point /= point[3]
        # direction: unit eigenvector corresponding to eigenvalue 0
        w, V = np.linalg.eig(M33)
        i = np.where(abs(np.real(w)) < 1e-8)[0]
        if not len(i):
            raise ValueError("no eigenvector corresponding to eigenvalue 0")
        direction = np.real(V[:, i[0]]).squeeze()
        direction /= vector_norm(direction)
        # normal: unit eigenvector of M33.T corresponding to eigenvalue 0
        w, V = np.linalg.eig(M33.T)
        i = np.where(abs(np.real(w)) < 1e-8)[0]
        if len(i):
            # parallel projection
            normal = np.real(V[:, i[0]]).squeeze()
            normal /= vector_norm(normal)
            return point, normal, direction, None, False
        else:
            # orthogonal projection, where normal equals direction vector
            return point, direction, None, None, False
    else:
        # perspective projection
        i = np.where(abs(np.real(w)) > 1e-8)[0]
        if not len(i):
            raise ValueError(
                "no eigenvector not corresponding to eigenvalue 0")
        point = np.real(V[:, i[-1]]).squeeze()
        point /= point[3]
        normal = - M[3, :3]
        perspective = M[:3, 3] / np.dot(point[:3], normal)
        if pseudo:
            perspective -= normal
        return point, normal, None, perspective, pseudo


def clip_matrix(left, right, bottom, top, near, far, perspective=False):
    """Return matrix to obtain normalized device coordinates from frustum.

    The frustum bounds are axis-aligned along x (left, right),
    y (bottom, top) and z (near, far).

    Normalized device coordinates are in range [-1, 1] if coordinates are
    inside the frustum.

    If perspective is True the frustum is a truncated pyramid with the
    perspective point at origin and direction along z axis, otherwise an
    orthographic canonical view volume (a box).

    Homogeneous coordinates transformed by the perspective clip matrix
    need to be dehomogenized (divided by w coordinate).

    >>> frustum = np.random.rand(6)
    >>> frustum[1] += frustum[0]
    >>> frustum[3] += frustum[2]
    >>> frustum[5] += frustum[4]
    >>> M = clip_matrix(perspective=False, *frustum)
    >>> a = np.dot(M, [frustum[0], frustum[2], frustum[4], 1])
    >>> np.allclose(a, [-1., -1., -1.,  1.])
    True
    >>> b = np.dot(M, [frustum[1], frustum[3], frustum[5], 1])
    >>> np.allclose(b, [ 1.,  1.,  1.,  1.])
    True
    >>> M = clip_matrix(perspective=True, *frustum)
    >>> v = np.dot(M, [frustum[0], frustum[2], frustum[4], 1])
    >>> c = v / v[3]
    >>> np.allclose(c, [-1., -1., -1.,  1.])
    True
    >>> v = np.dot(M, [frustum[1], frustum[3], frustum[4], 1])
    >>> d = v / v[3]
    >>> np.allclose(d, [ 1.,  1., -1.,  1.])
    True

    """
    if left >= right or bottom >= top or near >= far:
        raise ValueError("invalid frustum")
    if perspective:
        if near <= _EPS:
            raise ValueError("invalid frustum: near <= 0")
        t = 2.0 * near
        M = [[t / (left - right), 0.0, (right + left) / (right - left), 0.0],
             [0.0, t / (bottom - top), (top + bottom) / (top - bottom), 0.0],
             [0.0, 0.0, (far + near) / (near - far), t * far / (far - near)],
             [0.0, 0.0, -1.0, 0.0]]
    else:
        M = [[2.0 / (right - left), 0.0, 0.0, (right + left) / (left - right)],
             [0.0, 2.0 / (top - bottom), 0.0, (top + bottom) / (bottom - top)],
             [0.0, 0.0, 2.0 / (far - near), (far + near) / (near - far)],
             [0.0, 0.0, 0.0, 1.0]]
    return np.array(M)


def shear_matrix(angle, direction, point, normal):
    """Return matrix to shear by angle along direction vector on shear plane.

    The shear plane is defined by a point and normal vector. The direction
    vector must be orthogonal to the plane's normal vector.

    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.

    >>> angle = (random.random() - 0.5) * 4*math.pi
    >>> direct = np.random.random(3) - 0.5
    >>> point = np.random.random(3) - 0.5
    >>> normal = np.cross(direct, np.random.random(3))
    >>> S = shear_matrix(angle, direct, point, normal)
    >>> np.allclose(1, np.linalg.det(S))
    True

    """
    normal = unit_vector(normal[:3])
    direction = unit_vector(direction[:3])
    if abs(np.dot(normal, direction)) > 1e-6:
        raise ValueError("direction and normal vectors are not orthogonal")
    angle = math.tan(angle)
    M = np.identity(4)
    M[:3, :3] += angle * np.outer(direction, normal)
    M[:3, 3] = -angle * np.dot(point[:3], normal) * direction
    return M


def shear_from_matrix(matrix):
    """Return shear angle, direction and plane from shear matrix.

    >>> angle  = np.pi / 2.0
    >>> direct = [0.0, 1.0, 0.0]
    >>> point  = [0.0, 0.0, 0.0]
    >>> normal = np.cross(direct, np.roll(direct,1))
    >>> S0 = shear_matrix(angle, direct, point, normal)
    >>> angle, direct, point, normal = shear_from_matrix(S0)
    >>> S1 = shear_matrix(angle, direct, point, normal)
    >>> is_same_transform(S0, S1)
    True

    """
    M = np.array(matrix, dtype=np.float64, copy=False)
    M33 = M[:3, :3]
    # normal: cross independent eigenvectors corresponding to the eigenvalue 1
    w, V = np.linalg.eig(M33)

    i = np.where(abs(np.real(w) - 1.0) < 1e-4)[0]
    if len(i) < 2:
        raise ValueError("no two linear independent eigenvectors found %s" % w)
    V = np.real(V[:, i]).squeeze().T
    lenorm = -1.0
    for i0, i1 in ((0, 1), (0, 2), (1, 2)):
        n = np.cross(V[i0], V[i1])
        w = vector_norm(n)
        if w > lenorm:
            lenorm = w
            normal = n
    normal /= lenorm
    # direction and angle
    direction = np.dot(M33 - np.identity(3), normal)
    angle = vector_norm(direction)
    direction /= angle
    angle = math.atan(angle)
    # point: eigenvector corresponding to eigenvalue 1
    w, V = np.linalg.eig(M)

    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
    if not len(i):
        raise ValueError("no eigenvector corresponding to eigenvalue 1")
    point = np.real(V[:, i[-1]]).squeeze()
    point /= point[3]
    return angle, direction, point, normal


def decompose_matrix(matrix):
    """Return sequence of transformations from transformation matrix.

    matrix : array_like
        Non-degenerative homogeneous transformation matrix

    Return tuple of:
        scale : vector of 3 scaling factors
        shear : list of shear factors for x-y, x-z, y-z axes
        angles : list of Euler angles about static x, y, z axes
        translate : translation vector along x, y, z axes
        perspective : perspective partition of matrix

    Raise ValueError if matrix is of wrong type or degenerative.

    >>> T0 = translation_matrix([1, 2, 3])
    >>> scale, shear, angles, trans, persp = decompose_matrix(T0)
    >>> T1 = translation_matrix(trans)
    >>> np.allclose(T0, T1)
    True
    >>> S = scale_matrix(0.123)
    >>> scale, shear, angles, trans, persp = decompose_matrix(S)
    >>> scale[0]
    0.123
    >>> R0 = euler_matrix(1, 2, 3)
    >>> scale, shear, angles, trans, persp = decompose_matrix(R0)
    >>> R1 = euler_matrix(*angles)
    >>> np.allclose(R0, R1)
    True

    """
    M = np.array(matrix, dtype=np.float64, copy=True).T
    if abs(M[3, 3]) < _EPS:
        raise ValueError("M[3, 3] is zero")
    M /= M[3, 3]
    P = M.copy()
    P[:, 3] = 0.0, 0.0, 0.0, 1.0
    if not np.linalg.det(P):
        raise ValueError("matrix is singular")

    scale = np.zeros((3, ))
    shear = [0.0, 0.0, 0.0]
    angles = [0.0, 0.0, 0.0]

    if any(abs(M[:3, 3]) > _EPS):
        perspective = np.dot(M[:, 3], np.linalg.inv(P.T))
        M[:, 3] = 0.0, 0.0, 0.0, 1.0
    else:
        perspective = np.array([0.0, 0.0, 0.0, 1.0])

    translate = M[3, :3].copy()
    M[3, :3] = 0.0

    row = M[:3, :3].copy()
    scale[0] = vector_norm(row[0])
    row[0] /= scale[0]
    shear[0] = np.dot(row[0], row[1])
    row[1] -= row[0] * shear[0]
    scale[1] = vector_norm(row[1])
    row[1] /= scale[1]
    shear[0] /= scale[1]
    shear[1] = np.dot(row[0], row[2])
    row[2] -= row[0] * shear[1]
    shear[2] = np.dot(row[1], row[2])
    row[2] -= row[1] * shear[2]
    scale[2] = vector_norm(row[2])
    row[2] /= scale[2]
    shear[1:] /= scale[2]

    if np.dot(row[0], np.cross(row[1], row[2])) < 0:
        np.negative(scale, scale)
        np.negative(row, row)

    angles[1] = math.asin(-row[0, 2])
    if math.cos(angles[1]):
        angles[0] = math.atan2(row[1, 2], row[2, 2])
        angles[2] = math.atan2(row[0, 1], row[0, 0])
    else:
        angles[0] = math.atan2(-row[2, 1], row[1, 1])
        angles[2] = 0.0

    return scale, shear, angles, translate, perspective


def compose_matrix(scale=None, shear=None, angles=None, translate=None,
                   perspective=None):
    """Return transformation matrix from sequence of transformations.

    This is the inverse of the decompose_matrix function.

    Sequence of transformations:
        scale : vector of 3 scaling factors
        shear : list of shear factors for x-y, x-z, y-z axes
        angles : list of Euler angles about static x, y, z axes
        translate : translation vector along x, y, z axes
        perspective : perspective partition of matrix

    >>> scale = np.random.random(3) - 0.5
    >>> shear = np.random.random(3) - 0.5
    >>> angles = (np.random.random(3) - 0.5) * (2*math.pi)
    >>> trans = np.random.random(3) - 0.5
    >>> persp = np.random.random(4) - 0.5
    >>> M0 = compose_matrix(scale, shear, angles, trans, persp)
    >>> result = decompose_matrix(M0)
    >>> M1 = compose_matrix(*result)
    >>> is_same_transform(M0, M1)
    True

    """
    M = np.identity(4)
    if perspective is not None:
        P = np.identity(4)
        P[3, :] = perspective[:4]
        M = np.dot(M, P)
    if translate is not None:
        T = np.identity(4)
        T[:3, 3] = translate[:3]
        M = np.dot(M, T)
    if angles is not None:
        R = euler_matrix(angles[0], angles[1], angles[2], 'sxyz')
        M = np.dot(M, R)
    if shear is not None:
        Z = np.identity(4)
        Z[1, 2] = shear[2]
        Z[0, 2] = shear[1]
        Z[0, 1] = shear[0]
        M = np.dot(M, Z)
    if scale is not None:
        S = np.identity(4)
        S[0, 0] = scale[0]
        S[1, 1] = scale[1]
        S[2, 2] = scale[2]
        M = np.dot(M, S)
    M /= M[3, 3]
    return M


def orthogonalization_matrix(lengths, angles):
    """Return orthogonalization matrix for crystallographic cell coordinates.

    Angles are expected in degrees.

    The de-orthogonalization matrix is the inverse.

    >>> O = orthogonalization_matrix([10, 10, 10], [90, 90, 90])
    >>> np.allclose(O[:3, :3], np.identity(3, float) * 10)
    True
    >>> O = orthogonalization_matrix([9.8, 12.0, 15.5], [87.2, 80.7, 69.7])
    >>> np.allclose(np.sum(O), 43.063229)
    True

    """
    a, b, c = lengths
    angles = np.radians(angles)
    sina, sinb, _ = np.sin(angles)
    cosa, cosb, cosg = np.cos(angles)
    co = (cosa * cosb - cosg) / (sina * sinb)
    return np.array([
        [a * sinb * math.sqrt(1.0 - co * co), 0.0, 0.0, 0.0],
        [-a * sinb * co, b * sina, 0.0, 0.0],
        [a * cosb, b * cosa, c, 0.0],
        [0.0, 0.0, 0.0, 1.0]])


def affine_matrix_from_points(v0, v1, shear=True, scale=True, usesvd=True):
    """Return affine transform matrix to register two point sets.

    v0 and v1 are shape (ndims, *) arrays of at least ndims non-homogeneous
    coordinates, where ndims is the dimensionality of the coordinate space.

    If shear is False, a similarity transformation matrix is returned.
    If also scale is False, a rigid/Euclidean transformation matrix
    is returned.

    By default the algorithm by Hartley and Zissermann [15] is used.
    If usesvd is True, similarity and Euclidean transformation matrices
    are calculated by minimizing the weighted sum of squared deviations
    (RMSD) according to the algorithm by Kabsch [8].
    Otherwise, and if ndims is 3, the quaternion based algorithm by Horn [9]
    is used, which is slower when using this Python implementation.

    The returned matrix performs rotation, translation and uniform scaling
    (if specified).

    >>> v0 = [[0, 1031, 1031, 0], [0, 0, 1600, 1600]]
    >>> v1 = [[675, 826, 826, 677], [55, 52, 281, 277]]
    >>> mat = affine_matrix_from_points(v0, v1)
    >>> T = translation_matrix(np.random.random(3)-0.5)
    >>> R = random_rotation_matrix(np.random.random(3))
    >>> S = scale_matrix(random.random())
    >>> M = concatenate_matrices(T, R, S)
    >>> v0 = (np.random.rand(4, 100) - 0.5) * 20
    >>> v0[3] = 1
    >>> v1 = np.dot(M, v0)
    >>> v0[:3] += np.random.normal(0, 1e-8, 300).reshape(3, -1)
    >>> M = affine_matrix_from_points(v0[:3], v1[:3])
    >>> check = np.allclose(v1, np.dot(M, v0))

    More examples in superimposition_matrix()

    """
    v0 = np.array(v0, dtype=np.float64, copy=True)
    v1 = np.array(v1, dtype=np.float64, copy=True)

    ndims = v0.shape[0]
    if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape:
        raise ValueError("input arrays are of wrong shape or type")

    # move centroids to origin
    t0 = -np.mean(v0, axis=1)
    M0 = np.identity(ndims + 1)
    M0[:ndims, ndims] = t0
    v0 += t0.reshape(ndims, 1)
    t1 = -np.mean(v1, axis=1)
    M1 = np.identity(ndims + 1)
    M1[:ndims, ndims] = t1
    v1 += t1.reshape(ndims, 1)

    if shear:
        # Affine transformation
        A = np.concatenate((v0, v1), axis=0)
        u, s, vh = np.linalg.svd(A.T)
        vh = vh[:ndims].T
        B = vh[:ndims]
        C = vh[ndims:2 * ndims]
        t = np.dot(C, np.linalg.pinv(B))
        t = np.concatenate((t, np.zeros((ndims, 1))), axis=1)
        M = np.vstack((t, ((0.0,) * ndims) + (1.0,)))
    elif usesvd or ndims != 3:
        # Rigid transformation via SVD of covariance matrix
        u, s, vh = np.linalg.svd(np.dot(v1, v0.T))
        # rotation matrix from SVD orthonormal bases
        R = np.dot(u, vh)
        if np.linalg.det(R) < 0.0:
            # R does not constitute right handed system
            R -= np.outer(u[:, ndims - 1], vh[ndims - 1, :] * 2.0)
            s[-1] *= -1.0
        # homogeneous transformation matrix
        M = np.identity(ndims + 1)
        M[:ndims, :ndims] = R
    else:
        # Rigid transformation matrix via quaternion
        # compute symmetric matrix N
        xx, yy, zz = np.sum(v0 * v1, axis=1)
        xy, yz, zx = np.sum(v0 * np.roll(v1, -1, axis=0), axis=1)
        xz, yx, zy = np.sum(v0 * np.roll(v1, -2, axis=0), axis=1)
        N = [[xx + yy + zz, 0.0, 0.0, 0.0],
             [yz - zy, xx - yy - zz, 0.0, 0.0],
             [zx - xz, xy + yx, yy - xx - zz, 0.0],
             [xy - yx, zx + xz, yz + zy, zz - xx - yy]]
        # quaternion: eigenvector corresponding to most positive eigenvalue
        w, V = np.linalg.eigh(N)
        q = V[:, np.argmax(w)]
        q /= vector_norm(q)  # unit quaternion
        # homogeneous transformation matrix
        M = quaternion_matrix(q)

    if scale and not shear:
        # Affine transformation; scale is ratio of RMS deviations from centroid
        v0 *= v0
        v1 *= v1
        M[:ndims, :ndims] *= math.sqrt(np.sum(v1) / np.sum(v0))

    # move centroids back
    M = np.dot(np.linalg.inv(M1), np.dot(M, M0))
    M /= M[ndims, ndims]
    return M


def superimposition_matrix(v0, v1, scale=False, usesvd=True):
    """Return matrix to transform given 3D point set into second point set.

    v0 and v1 are shape (3, *) or (4, *) arrays of at least 3 points.

    The parameters scale and usesvd are explained in the more general
    affine_matrix_from_points function.

    The returned matrix is a similarity or Euclidean transformation matrix.
    This function has a fast C implementation in transformations.c.

    >>> v0 = np.random.rand(3, 10)
    >>> M = superimposition_matrix(v0, v0)
    >>> np.allclose(M, np.identity(4))
    True
    >>> R = random_rotation_matrix(np.random.random(3))
    >>> v0 = [[1,0,0], [0,1,0], [0,0,1], [1,1,1]]
    >>> v1 = np.dot(R, v0)
    >>> M = superimposition_matrix(v0, v1)
    >>> np.allclose(v1, np.dot(M, v0))
    True
    >>> v0 = (np.random.rand(4, 100) - 0.5) * 20
    >>> v0[3] = 1
    >>> v1 = np.dot(R, v0)
    >>> M = superimposition_matrix(v0, v1)
    >>> np.allclose(v1, np.dot(M, v0))
    True
    >>> S = scale_matrix(random.random())
    >>> T = translation_matrix(np.random.random(3)-0.5)
    >>> M = concatenate_matrices(T, R, S)
    >>> v1 = np.dot(M, v0)
    >>> v0[:3] += np.random.normal(0, 1e-9, 300).reshape(3, -1)
    >>> M = superimposition_matrix(v0, v1, scale=True)
    >>> np.allclose(v1, np.dot(M, v0))
    True
    >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False)
    >>> np.allclose(v1, np.dot(M, v0))
    True
    >>> v = np.empty((4, 100, 3))
    >>> v[:, :, 0] = v0
    >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False)
    >>> np.allclose(v1, np.dot(M, v[:, :, 0]))
    True

    """
    v0 = np.array(v0, dtype=np.float64, copy=False)[:3]
    v1 = np.array(v1, dtype=np.float64, copy=False)[:3]
    return affine_matrix_from_points(v0, v1, shear=False,
                                     scale=scale, usesvd=usesvd)


def euler_matrix(ai, aj, ak, axes='sxyz'):
    """Return homogeneous rotation matrix from Euler angles and axis sequence.

    ai, aj, ak : Euler's roll, pitch and yaw angles
    axes : One of 24 axis sequences as string or encoded tuple

    >>> R = euler_matrix(1, 2, 3, 'syxz')
    >>> np.allclose(np.sum(R[0]), -1.34786452)
    True
    >>> R = euler_matrix(1, 2, 3, (0, 1, 0, 1))
    >>> np.allclose(np.sum(R[0]), -0.383436184)
    True
    >>> ai, aj, ak = (4*math.pi) * (np.random.random(3) - 0.5)
    >>> for axes in _AXES2TUPLE.keys():
    ...    R = euler_matrix(ai, aj, ak, axes)
    >>> for axes in _TUPLE2AXES.keys():
    ...    R = euler_matrix(ai, aj, ak, axes)

    """
    try:
        firstaxis, parity, repetition, frame = _AXES2TUPLE[axes]
    except (AttributeError, KeyError):
        _TUPLE2AXES[axes]  # validation
        firstaxis, parity, repetition, frame = axes

    i = firstaxis
    j = _NEXT_AXIS[i + parity]
    k = _NEXT_AXIS[i - parity + 1]

    if frame:
        ai, ak = ak, ai
    if parity:
        ai, aj, ak = -ai, -aj, -ak

    si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak)
    ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak)
    cc, cs = ci * ck, ci * sk
    sc, ss = si * ck, si * sk

    M = np.identity(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_from_matrix(matrix, axes='sxyz'):
    """Return Euler angles from rotation matrix for specified axis sequence.

    axes : One of 24 axis sequences as string or encoded tuple

    Note that many Euler angle triplets can describe one matrix.

    >>> R0 = euler_matrix(1, 2, 3, 'syxz')
    >>> al, be, ga = euler_from_matrix(R0, 'syxz')
    >>> R1 = euler_matrix(al, be, ga, 'syxz')
    >>> np.allclose(R0, R1)
    True
    >>> angles = (4*math.pi) * (np.random.random(3) - 0.5)
    >>> for axes in _AXES2TUPLE.keys():
    ...    R0 = euler_matrix(axes=axes, *angles)
    ...    R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes))
    ...    if not np.allclose(R0, R1): print(axes, "failed")

    """
    try:
        firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
    except (AttributeError, KeyError):
        _TUPLE2AXES[axes]  # validation
        firstaxis, parity, repetition, frame = axes

    i = firstaxis
    j = _NEXT_AXIS[i + parity]
    k = _NEXT_AXIS[i - parity + 1]

    M = np.array(matrix, dtype=np.float64, copy=False)[:3, :3]
    if repetition:
        sy = math.sqrt(M[i, j] * M[i, j] + M[i, k] * M[i, k])
        if sy > _EPS:
            ax = math.atan2(M[i, j], M[i, k])
            ay = math.atan2(sy, M[i, i])
            az = math.atan2(M[j, i], -M[k, i])
        else:
            ax = math.atan2(-M[j, k], M[j, j])
            ay = math.atan2(sy, M[i, i])
            az = 0.0
    else:
        cy = math.sqrt(M[i, i] * M[i, i] + M[j, i] * M[j, i])
        if cy > _EPS:
            ax = math.atan2(M[k, j], M[k, k])
            ay = math.atan2(-M[k, i], cy)
            az = math.atan2(M[j, i], M[i, i])
        else:
            ax = math.atan2(-M[j, k], M[j, j])
            ay = math.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 euler_from_quaternion(quaternion, axes='sxyz'):
    """Return Euler angles from quaternion for specified axis sequence.

    >>> angles = euler_from_quaternion([0.99810947, 0.06146124, 0, 0])
    >>> np.allclose(angles, [0.123, 0, 0])
    True

    """
    return euler_from_matrix(quaternion_matrix(quaternion), axes)


def quaternion_from_euler(ai, aj, ak, axes='sxyz'):
    """Return quaternion from Euler angles and axis sequence.

    ai, aj, ak : Euler's roll, pitch and yaw angles
    axes : One of 24 axis sequences as string or encoded tuple

    >>> q = quaternion_from_euler(1, 2, 3, 'ryxz')
    >>> np.allclose(q, [0.435953, 0.310622, -0.718287, 0.444435])
    True

    """
    try:
        firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
    except (AttributeError, KeyError):
        _TUPLE2AXES[axes]  # validation
        firstaxis, parity, repetition, frame = axes

    i = firstaxis + 1
    j = _NEXT_AXIS[i + parity - 1] + 1
    k = _NEXT_AXIS[i - parity] + 1

    if frame:
        ai, ak = ak, ai
    if parity:
        aj = -aj

    ai /= 2.0
    aj /= 2.0
    ak /= 2.0
    ci = math.cos(ai)
    si = math.sin(ai)
    cj = math.cos(aj)
    sj = math.sin(aj)
    ck = math.cos(ak)
    sk = math.sin(ak)
    cc = ci * ck
    cs = ci * sk
    sc = si * ck
    ss = si * sk

    q = np.empty((4, ))
    if repetition:
        q[0] = cj * (cc - ss)
        q[i] = cj * (cs + sc)
        q[j] = sj * (cc + ss)
        q[k] = sj * (cs - sc)
    else:
        q[0] = cj * cc + sj * ss
        q[i] = cj * sc - sj * cs
        q[j] = cj * ss + sj * cc
        q[k] = cj * cs - sj * sc
    if parity:
        q[j] *= -1.0

    return q


def quaternion_about_axis(angle, axis):
    """Return quaternion for rotation about axis.

    >>> q = quaternion_about_axis(0.123, [1, 0, 0])
    >>> np.allclose(q, [0.99810947, 0.06146124, 0, 0])
    True

    """
    q = np.array([0.0, axis[0], axis[1], axis[2]])
    qlen = vector_norm(q)
    if qlen > _EPS:
        q *= math.sin(angle / 2.0) / qlen
    q[0] = math.cos(angle / 2.0)
    return q


def quaternion_matrix(quaternion):
    """
    Return a homogeneous rotation matrix from quaternion.

    >>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0])
    >>> np.allclose(M, rotation_matrix(0.123, [1, 0, 0]))
    True
    >>> M = quaternion_matrix([1, 0, 0, 0])
    >>> np.allclose(M, np.identity(4))
    True
    >>> M = quaternion_matrix([0, 1, 0, 0])
    >>> np.allclose(M, np.diag([1, -1, -1, 1]))
    True
    >>> M = quaternion_matrix([[1, 0, 0, 0],[0, 1, 0, 0]])
    >>> np.allclose(M, np.array([np.identity(4), np.diag([1, -1, -1, 1])]))
    True


    """
    q = np.array(quaternion,
                 dtype=np.float64,
                 copy=True).reshape((-1, 4))
    n = np.einsum('ij,ij->i', q, q)
    # how many entries do we have
    num_qs = len(n)
    identities = n < _EPS
    q[~identities, :] *= np.sqrt(2.0 / n[~identities, None])
    q = np.einsum('ij,ik->ikj', q, q)

    # store the result
    ret = np.zeros((num_qs, 4, 4))

    # pack the values into the result
    ret[:, 0, 0] = 1.0 - q[:, 2, 2] - q[:, 3, 3]
    ret[:, 0, 1] = q[:, 1, 2] - q[:, 3, 0]
    ret[:, 0, 2] = q[:, 1, 3] + q[:, 2, 0]
    ret[:, 1, 0] = q[:, 1, 2] + q[:, 3, 0]
    ret[:, 1, 1] = 1.0 - q[:, 1, 1] - q[:, 3, 3]
    ret[:, 1, 2] = q[:, 2, 3] - q[:, 1, 0]
    ret[:, 2, 0] = q[:, 1, 3] - q[:, 2, 0]
    ret[:, 2, 1] = q[:, 2, 3] + q[:, 1, 0]
    ret[:, 2, 2] = 1.0 - q[:, 1, 1] - q[:, 2, 2]
    ret[:, 3, 3] = 1.0
    # set any identities
    ret[identities] = np.eye(4)[None, ...]

    return ret.squeeze()


def quaternion_from_matrix(matrix, isprecise=False):
    """Return quaternion from rotation matrix.

    If isprecise is True, the input matrix is assumed to be a precise rotation
    matrix and a faster algorithm is used.

    >>> q = quaternion_from_matrix(np.identity(4), True)
    >>> np.allclose(q, [1, 0, 0, 0])
    True
    >>> q = quaternion_from_matrix(np.diag([1, -1, -1, 1]))
    >>> np.allclose(q, [0, 1, 0, 0]) or np.allclose(q, [0, -1, 0, 0])
    True
    >>> R = rotation_matrix(0.123, (1, 2, 3))
    >>> q = quaternion_from_matrix(R, True)
    >>> np.allclose(q, [0.9981095, 0.0164262, 0.0328524, 0.0492786])
    True
    >>> R = [[-0.545, 0.797, 0.260, 0], [0.733, 0.603, -0.313, 0],
    ...      [-0.407, 0.021, -0.913, 0], [0, 0, 0, 1]]
    >>> q = quaternion_from_matrix(R)
    >>> np.allclose(q, [0.19069, 0.43736, 0.87485, -0.083611])
    True
    >>> R = [[0.395, 0.362, 0.843, 0], [-0.626, 0.796, -0.056, 0],
    ...      [-0.677, -0.498, 0.529, 0], [0, 0, 0, 1]]
    >>> q = quaternion_from_matrix(R)
    >>> np.allclose(q, [0.82336615, -0.13610694, 0.46344705, -0.29792603])
    True
    >>> R = random_rotation_matrix()
    >>> q = quaternion_from_matrix(R)
    >>> is_same_transform(R, quaternion_matrix(q))
    True
    >>> is_same_quaternion(quaternion_from_matrix(R, isprecise=False),
    ...                    quaternion_from_matrix(R, isprecise=True))
    True
    >>> R = euler_matrix(0.0, 0.0, np.pi/2.0)
    >>> is_same_quaternion(quaternion_from_matrix(R, isprecise=False),
    ...                    quaternion_from_matrix(R, isprecise=True))
    True

    """
    M = np.array(matrix, dtype=np.float64, copy=False)[:4, :4]
    if isprecise:
        q = np.empty((4, ))
        t = np.trace(M)
        if t > M[3, 3]:
            q[0] = t
            q[3] = M[1, 0] - M[0, 1]
            q[2] = M[0, 2] - M[2, 0]
            q[1] = M[2, 1] - M[1, 2]
        else:
            i, j, k = 0, 1, 2
            if M[1, 1] > M[0, 0]:
                i, j, k = 1, 2, 0
            if M[2, 2] > M[i, i]:
                i, j, k = 2, 0, 1
            t = M[i, i] - (M[j, j] + M[k, k]) + M[3, 3]
            q[i] = t
            q[j] = M[i, j] + M[j, i]
            q[k] = M[k, i] + M[i, k]
            q[3] = M[k, j] - M[j, k]
            q = q[[3, 0, 1, 2]]
        q *= 0.5 / math.sqrt(t * M[3, 3])
    else:
        m00 = M[0, 0]
        m01 = M[0, 1]
        m02 = M[0, 2]
        m10 = M[1, 0]
        m11 = M[1, 1]
        m12 = M[1, 2]
        m20 = M[2, 0]
        m21 = M[2, 1]
        m22 = M[2, 2]
        # symmetric matrix K
        K = np.array([[m00 - m11 - m22, 0.0, 0.0, 0.0],
                      [m01 + m10, m11 - m00 - m22, 0.0, 0.0],
                      [m02 + m20, m12 + m21, m22 - m00 - m11, 0.0],
                      [m21 - m12, m02 - m20, m10 - m01, m00 + m11 + m22]])
        K /= 3.0
        # quaternion is eigenvector of K that corresponds to largest eigenvalue
        w, V = np.linalg.eigh(K)
        q = V[[3, 0, 1, 2], np.argmax(w)]
    if q[0] < 0.0:
        np.negative(q, q)
    return q


def quaternion_multiply(quaternion1, quaternion0):
    """Return multiplication of two quaternions.

    >>> q = quaternion_multiply([4, 1, -2, 3], [8, -5, 6, 7])
    >>> np.allclose(q, [28, -44, -14, 48])
    True

    """
    w0, x0, y0, z0 = quaternion0
    w1, x1, y1, z1 = quaternion1
    return np.array([-x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
                     x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
                     -x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
                     x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0], dtype=np.float64)


def quaternion_conjugate(quaternion):
    """Return conjugate of quaternion.

    >>> q0 = random_quaternion()
    >>> q1 = quaternion_conjugate(q0)
    >>> q1[0] == q0[0] and all(q1[1:] == -q0[1:])
    True

    """
    q = np.array(quaternion, dtype=np.float64, copy=True)
    np.negative(q[1:], q[1:])
    return q


def quaternion_inverse(quaternion):
    """Return inverse of quaternion.

    >>> q0 = random_quaternion()
    >>> q1 = quaternion_inverse(q0)
    >>> np.allclose(quaternion_multiply(q0, q1), [1, 0, 0, 0])
    True

    """
    q = np.array(quaternion, dtype=np.float64, copy=True)
    np.negative(q[1:], q[1:])
    return q / np.dot(q, q)


def quaternion_real(quaternion):
    """Return real part of quaternion.

    >>> quaternion_real([3, 0, 1, 2])
    3.0

    """
    return float(quaternion[0])


def quaternion_imag(quaternion):
    """Return imaginary part of quaternion.

    >>> quaternion_imag([3, 0, 1, 2])
    array([0., 1., 2.])

    """
    return np.array(quaternion[1:4], dtype=np.float64, copy=True)


def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True):
    """Return spherical linear interpolation between two quaternions.

    >>> q0 = random_quaternion()
    >>> q1 = random_quaternion()
    >>> q = quaternion_slerp(q0, q1, 0)
    >>> np.allclose(q, q0)
    True
    >>> q = quaternion_slerp(q0, q1, 1, 1)
    >>> np.allclose(q, q1)
    True
    >>> q = quaternion_slerp(q0, q1, 0.5)
    >>> angle = math.acos(np.dot(q0, q))
    >>> np.allclose(2, math.acos(np.dot(q0, q1)) / angle) or \
        np.allclose(2, math.acos(-np.dot(q0, q1)) / angle)
    True

    """
    q0 = unit_vector(quat0[:4])
    q1 = unit_vector(quat1[:4])
    if fraction == 0.0:
        return q0
    elif fraction == 1.0:
        return q1
    d = np.dot(q0, q1)
    if abs(abs(d) - 1.0) < _EPS:
        return q0
    if shortestpath and d < 0.0:
        # invert rotation
        d = -d
        np.negative(q1, q1)
    angle = math.acos(d) + spin * math.pi
    if abs(angle) < _EPS:
        return q0
    isin = 1.0 / math.sin(angle)
    q0 *= math.sin((1.0 - fraction) * angle) * isin
    q1 *= math.sin(fraction * angle) * isin
    q0 += q1
    return q0


def random_quaternion(rand=None, num=1):
    """Return uniform random unit quaternion.

    rand: array like or None
        Three independent random variables that are uniformly distributed
        between 0 and 1.

    >>> q = random_quaternion()
    >>> np.allclose(1, vector_norm(q))
    True
    >>> q = random_quaternion(num=10)
    >>> np.allclose(1, vector_norm(q, axis=1))
    True
    >>> q = random_quaternion(np.random.random(3))
    >>> len(q.shape), q.shape[0]==4
    (1, True)

    """
    if rand is None:
        rand = np.random.rand(3 * num).reshape((3, -1))
    else:
        assert rand.shape[0] == 3
    r1 = np.sqrt(1.0 - rand[0])
    r2 = np.sqrt(rand[0])
    pi2 = math.pi * 2.0
    t1 = pi2 * rand[1]
    t2 = pi2 * rand[2]
    return np.array([np.cos(t2) * r2, np.sin(t1) * r1,
                     np.cos(t1) * r1, np.sin(t2) * r2]).T.squeeze()


def random_rotation_matrix(rand=None, num=1, translate=False):
    """Return uniform random rotation matrix.

    rand: array like
        Three independent random variables that are uniformly distributed
        between 0 and 1 for each returned quaternion.

    >>> R = random_rotation_matrix()
    >>> np.allclose(np.dot(R.T, R), np.identity(4))
    True
    >>> R = random_rotation_matrix(num=10)
    >>> np.allclose(np.einsum('...ji,...jk->...ik', R, R), np.identity(4))
    True

    """
    matrix = quaternion_matrix(
        random_quaternion(rand=rand, num=num))
    if translate:
        scale = float(translate)
        matrix[:3, 3] = (np.random.random(3) - 0.5) * scale
    return matrix


class Arcball(object):
    """Virtual Trackball Control.

    >>> ball = Arcball()
    >>> ball = Arcball(initial=np.identity(4))
    >>> ball.place([320, 320], 320)
    >>> ball.down([500, 250])
    >>> ball.drag([475, 275])
    >>> R = ball.matrix()
    >>> np.allclose(np.sum(R), 3.90583455)
    True
    >>> ball = Arcball(initial=[1, 0, 0, 0])
    >>> ball.place([320, 320], 320)
    >>> ball.setaxes([1, 1, 0], [-1, 1, 0])
    >>> ball.constrain = True
    >>> ball.down([400, 200])
    >>> ball.drag([200, 400])
    >>> R = ball.matrix()
    >>> np.allclose(np.sum(R), 0.2055924)
    True
    >>> ball.next()

    """

    def __init__(self, initial=None):
        """Initialize virtual trackball control.

        initial : quaternion or rotation matrix

        """
        self._axis = None
        self._axes = None
        self._radius = 1.0
        self._center = [0.0, 0.0]
        self._vdown = np.array([0.0, 0.0, 1.0])
        self._constrain = False
        if initial is None:
            self._qdown = np.array([1.0, 0.0, 0.0, 0.0])
        else:
            initial = np.array(initial, dtype=np.float64)
            if initial.shape == (4, 4):
                self._qdown = quaternion_from_matrix(initial)
            elif initial.shape == (4, ):
                initial /= vector_norm(initial)
                self._qdown = initial
            else:
                raise ValueError("initial not a quaternion or matrix")
        self._qnow = self._qpre = self._qdown

    def place(self, center, radius):
        """Place Arcball, e.g. when window size changes.

        center : sequence[2]
            Window coordinates of trackball center.
        radius : float
            Radius of trackball in window coordinates.

        """
        self._radius = float(radius)
        self._center[0] = center[0]
        self._center[1] = center[1]

    def setaxes(self, *axes):
        """Set axes to constrain rotations."""
        if axes is None:
            self._axes = None
        else:
            self._axes = [unit_vector(axis) for axis in axes]

    @property
    def constrain(self):
        """Return state of constrain to axis mode."""
        return self._constrain

    @constrain.setter
    def constrain(self, value):
        """Set state of constrain to axis mode."""
        self._constrain = bool(value)

    def down(self, point):
        """Set initial cursor window coordinates and pick constrain-axis."""
        self._vdown = arcball_map_to_sphere(point, self._center, self._radius)
        self._qdown = self._qpre = self._qnow
        if self._constrain and self._axes is not None:
            self._axis = arcball_nearest_axis(self._vdown, self._axes)
            self._vdown = arcball_constrain_to_axis(self._vdown, self._axis)
        else:
            self._axis = None

    def drag(self, point):
        """Update current cursor window coordinates."""
        vnow = arcball_map_to_sphere(point, self._center, self._radius)
        if self._axis is not None:
            vnow = arcball_constrain_to_axis(vnow, self._axis)
        self._qpre = self._qnow
        t = np.cross(self._vdown, vnow)
        if np.dot(t, t) < _EPS:
            self._qnow = self._qdown
        else:
            q = [np.dot(self._vdown, vnow), t[0], t[1], t[2]]
            self._qnow = quaternion_multiply(q, self._qdown)

    def next(self, acceleration=0.0):
        """Continue rotation in direction of last drag."""
        q = quaternion_slerp(self._qpre, self._qnow, 2.0 + acceleration, False)
        self._qpre, self._qnow = self._qnow, q

    def matrix(self):
        """Return homogeneous rotation matrix."""
        return quaternion_matrix(self._qnow)


def arcball_map_to_sphere(point, center, radius):
    """Return unit sphere coordinates from window coordinates."""
    v0 = (point[0] - center[0]) / radius
    v1 = (center[1] - point[1]) / radius
    n = v0 * v0 + v1 * v1
    if n > 1.0:
        # position outside of sphere
        n = math.sqrt(n)
        return np.array([v0 / n, v1 / n, 0.0])
    else:
        return np.array([v0, v1, math.sqrt(1.0 - n)])


def arcball_constrain_to_axis(point, axis):
    """Return sphere point perpendicular to axis."""
    v = np.array(point, dtype=np.float64, copy=True)
    a = np.array(axis, dtype=np.float64, copy=True)
    v -= a * np.dot(a, v)  # on plane
    n = vector_norm(v)
    if n > _EPS:
        if v[2] < 0.0:
            np.negative(v, v)
        v /= n
        return v
    if a[2] == 1.0:
        return np.array([1.0, 0.0, 0.0])
    return unit_vector([-a[1], a[0], 0.0])


def arcball_nearest_axis(point, axes):
    """Return axis, which arc is nearest to point."""
    point = np.array(point, dtype=np.float64, copy=False)
    nearest = None
    mx = -1.0
    for axis in axes:
        t = np.dot(arcball_constrain_to_axis(point, axis), point)
        if t > mx:
            nearest = axis
            mx = t
    return nearest


# epsilon for testing whether a number is close to zero
_EPS = np.finfo(float).eps * 4.0

# axis sequences for Euler angles
_NEXT_AXIS = [1, 2, 0, 1]

# map axes strings to/from tuples of inner axis, parity, repetition, frame
_AXES2TUPLE = {
    '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)}

_TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items())


def vector_norm(data, axis=None, out=None):
    """Return length, i.e. Euclidean norm, of ndarray along axis.

    >>> v = np.random.random(3)
    >>> n = vector_norm(v)
    >>> np.allclose(n, np.linalg.norm(v))
    True
    >>> v = np.random.rand(6, 5, 3)
    >>> n = vector_norm(v, axis=-1)
    >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=2)))
    True
    >>> n = vector_norm(v, axis=1)
    >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1)))
    True
    >>> v = np.random.rand(5, 4, 3)
    >>> n = np.empty((5, 3))
    >>> vector_norm(v, axis=1, out=n)
    >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1)))
    True
    >>> vector_norm([])
    0.0
    >>> vector_norm([1])
    1.0

    """
    data = np.array(data, dtype=np.float64, copy=True)
    if out is None:
        if data.ndim == 1:
            return math.sqrt(np.dot(data, data))
        data *= data
        out = np.atleast_1d(np.sum(data, axis=axis))
        np.sqrt(out, out)
        return out
    else:
        data *= data
        np.sum(data, axis=axis, out=out)
        np.sqrt(out, out)


def unit_vector(data, axis=None, out=None):
    """Return ndarray normalized by length, i.e. Euclidean norm, along axis.

    >>> v0 = np.random.random(3)
    >>> v1 = unit_vector(v0)
    >>> np.allclose(v1, v0 / np.linalg.norm(v0))
    True
    >>> v0 = np.random.rand(5, 4, 3)
    >>> v1 = unit_vector(v0, axis=-1)
    >>> v2 = v0 / np.expand_dims(np.sqrt(np.sum(v0*v0, axis=2)), 2)
    >>> np.allclose(v1, v2)
    True
    >>> v1 = unit_vector(v0, axis=1)
    >>> v2 = v0 / np.expand_dims(np.sqrt(np.sum(v0*v0, axis=1)), 1)
    >>> np.allclose(v1, v2)
    True
    >>> v1 = np.empty((5, 4, 3))
    >>> unit_vector(v0, axis=1, out=v1)
    >>> np.allclose(v1, v2)
    True
    >>> list(unit_vector([]))
    []
    >>> list(unit_vector([1]))
    [1.0]

    """
    if out is None:
        data = np.array(data, dtype=np.float64, copy=True)
        if data.ndim == 1:
            data /= math.sqrt(np.dot(data, data))
            return data
    else:
        if out is not data:
            out[:] = np.array(data, copy=False)
        data = out
    length = np.atleast_1d(np.sum(data * data, axis))
    np.sqrt(length, length)
    if axis is not None:
        length = np.expand_dims(length, axis)
    data /= length
    if out is None:
        return data


def random_vector(size):
    """Return array of random doubles in the half-open interval [0.0, 1.0).

    >>> v = random_vector(10000)
    >>> np.all(v >= 0) and np.all(v < 1)
    True
    >>> v0 = random_vector(10)
    >>> v1 = random_vector(10)
    >>> np.any(v0 == v1)
    False

    """
    return np.random.random(size)


def vector_product(v0, v1, axis=0):
    """Return vector perpendicular to vectors.

    >>> v = vector_product([2, 0, 0], [0, 3, 0])
    >>> np.allclose(v, [0, 0, 6])
    True
    >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]]
    >>> v1 = [[3], [0], [0]]
    >>> v = vector_product(v0, v1)
    >>> np.allclose(v, [[0, 0, 0, 0], [0, 0, 6, 6], [0, -6, 0, -6]])
    True
    >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]]
    >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]]
    >>> v = vector_product(v0, v1, axis=1)
    >>> np.allclose(v, [[0, 0, 6], [0, -6, 0], [6, 0, 0], [0, -6, 6]])
    True

    """
    return np.cross(v0, v1, axis=axis)


def angle_between_vectors(v0, v1, directed=True, axis=0):
    """Return angle between vectors.

    If directed is False, the input vectors are interpreted as undirected axes,
    i.e. the maximum angle is pi/2.

    >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3])
    >>> np.allclose(a, math.pi)
    True
    >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3], directed=False)
    >>> np.allclose(a, 0)
    True
    >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]]
    >>> v1 = [[3], [0], [0]]
    >>> a = angle_between_vectors(v0, v1)
    >>> np.allclose(a, [0, 1.5708, 1.5708, 0.95532])
    True
    >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]]
    >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]]
    >>> a = angle_between_vectors(v0, v1, axis=1)
    >>> np.allclose(a, [1.5708, 1.5708, 1.5708, 0.95532])
    True

    """
    v0 = np.array(v0, dtype=np.float64, copy=False)
    v1 = np.array(v1, dtype=np.float64, copy=False)
    dot = np.sum(v0 * v1, axis=axis)
    dot /= vector_norm(v0, axis=axis) * vector_norm(v1, axis=axis)
    return np.arccos(dot if directed else np.fabs(dot))


def inverse_matrix(matrix):
    """Return inverse of square transformation matrix.

    >>> M0 = random_rotation_matrix()
    >>> M1 = inverse_matrix(M0.T)
    >>> np.allclose(M1, np.linalg.inv(M0.T))
    True
    >>> for size in range(1, 7):
    ...     M0 = np.random.rand(size, size)
    ...     M1 = inverse_matrix(M0)
    ...     if not np.allclose(M1, np.linalg.inv(M0)): print(size)

    """
    return np.linalg.inv(matrix)


def concatenate_matrices(*matrices):
    """Return concatenation of series of transformation matrices.

    >>> M = np.random.rand(16).reshape((4, 4)) - 0.5
    >>> np.allclose(M, concatenate_matrices(M))
    True
    >>> np.allclose(np.dot(M, M.T), concatenate_matrices(M, M.T))
    True

    """
    M = np.identity(4)
    for i in matrices:
        M = np.dot(M, i)
    return M


def is_same_transform(matrix0, matrix1):
    """Return True if two matrices perform same transformation.

    >>> is_same_transform(np.identity(4), np.identity(4))
    True
    >>> is_same_transform(np.identity(4), random_rotation_matrix())
    False

    """
    matrix0 = np.array(matrix0, dtype=np.float64, copy=True)
    matrix0 /= matrix0[3, 3]
    matrix1 = np.array(matrix1, dtype=np.float64, copy=True)
    matrix1 /= matrix1[3, 3]
    return np.allclose(matrix0, matrix1)


def is_same_quaternion(q0, q1):
    """Return True if two quaternions are equal."""
    q0 = np.array(q0)
    q1 = np.array(q1)
    return np.allclose(q0, q1) or np.allclose(q0, -q1)


def transform_around(matrix, point):
    """
    Given a transformation matrix, apply its rotation
    around a point in space.

    Parameters
    ----------
    matrix: (4,4) or (3, 3) float, transformation matrix
    point:  (3,) or (2,)  float, point in space

    Returns
    ---------
    result: (4,4) transformation matrix
    """
    point = np.asanyarray(point)
    matrix = np.asanyarray(matrix)
    dim = len(point)
    if matrix.shape != (dim + 1,
                        dim + 1):
        raise ValueError('matrix must be (d+1, d+1)')

    translate = np.eye(dim + 1)
    translate[:dim, dim] = -point
    result = np.dot(matrix, translate)
    translate[:dim, dim] = point
    result = np.dot(translate, result)

    return result


def planar_matrix(offset=None,
                  theta=None,
                  point=None,
                  scale=None):
    """
    2D homogeonous transformation matrix.

    Parameters
    ----------
    offset : (2,) float
      XY offset
    theta : float
      Rotation around Z in radians
    point :  (2, ) float
      Point to rotate around
    scale : (2,) float or None
      Scale to apply

    Returns
    ----------
    matrix : (3, 3) flat
      Homogeneous 2D transformation matrix
    """
    if offset is None:
        offset = [0.0, 0.0]
    if theta is None:
        theta = 0.0
    offset = np.asanyarray(offset, dtype=np.float64)
    theta = float(theta)
    if not np.isfinite(theta):
        raise ValueError('theta must be finite angle!')
    if offset.shape != (2,):
        raise ValueError('offset must be length 2!')

    T = np.eye(3, dtype=np.float64)
    s = np.sin(theta)
    c = np.cos(theta)

    T[0, :2] = [c, s]
    T[1, :2] = [-s, c]
    T[:2, 2] = offset

    if point is not None:
        T = transform_around(matrix=T, point=point)

    if scale is not None:
        S = np.eye(3)
        S[:2, :2] *= scale
        T = np.dot(S, T)

    return T


def planar_matrix_to_3D(matrix_2D):
    """
    Given a 2D homogeneous rotation matrix convert it to a 3D rotation
    matrix that is rotating around the Z axis

    Parameters
    ----------
    matrix_2D: (3,3) float, homogeneous 2D rotation matrix

    Returns
    ----------
    matrix_3D: (4,4) float, homogeneous 3D rotation matrix
    """

    matrix_2D = np.asanyarray(matrix_2D, dtype=np.float64)
    if matrix_2D.shape != (3, 3):
        raise ValueError('Homogenous 2D transformation matrix required!')

    matrix_3D = np.eye(4)
    # translation
    matrix_3D[:2, 3] = matrix_2D[:2, 2]
    # rotation from 2D to around Z
    matrix_3D[:2, :2] = matrix_2D[:2, :2]

    return matrix_3D


def spherical_matrix(theta, phi, axes='sxyz'):
    """
    Give a spherical coordinate vector, find the rotation that will
    transform a [0,0,1] vector to those coordinates

    Parameters
    -----------
    theta: float, rotation angle in radians
    phi:   float, rotation angle in radians

    Returns
    ----------
    matrix: (4,4) rotation matrix where the following will
             be a cartesian vector in the direction of the
             input spherical coordinates:
                np.dot(matrix, [0,0,1,0])

    """
    result = euler_matrix(0.0, phi, theta, axes=axes)
    return result


def transform_points(points,
                     matrix,
                     translate=True):
    """
    Returns points rotated by a homogeneous
    transformation matrix.

    If points are (n, 2) matrix must be (3, 3)
    If points are (n, 3) matrix must be (4, 4)

    Parameters
    ----------
    points : (n, dim) float
      Points where `dim` is 2 or 3.
    matrix : (3, 3) or (4, 4) float
      Homogeneous rotation matrix.
    translate : bool
      Apply translation from matrix or not.

    Returns
    ----------
    transformed : (n, dim) float
      Transformed points.
    """
    points = np.asanyarray(points, dtype=np.float64)
    if len(points) == 0:
        return points.copy()

    # check the matrix against the points
    matrix = np.asanyarray(matrix, dtype=np.float64)
    # shorthand the shape
    count, dim = points.shape

    # quickly check to see if we've been passed an identity matrix
    if np.abs(matrix - _IDENTITY[:dim + 1, :dim + 1]).max() < 1e-8:
        return np.ascontiguousarray(points.copy())

    if translate:
        # apply translation and rotation
        stack = np.column_stack((points, np.ones(count)))
        return np.dot(matrix, stack.T).T[:, :dim]

    # only apply the rotation
    return np.dot(matrix[:dim, :dim], points.T).T


def fix_rigid(matrix, max_deviance=1e-5):
    """
    If a homogeneous transformation matrix is *almost* a rigid
    transform but many matrix-multiplies have accumulated some
    floating point error try to restore the matrix using SVD.

    Parameters
    -----------
    matrix : (4, 4) or (3, 3) float
      Homogeneous transformation matrix.
    max_deviance : float
      Do not alter the matrix if it is not rigid by more
      than this amount.

    Returns
    ----------
    repaired : (4, 4) or (3, 3) float
      Repaired homogeneous transformation matrix
    """
    dim = matrix.shape[0] - 1
    check = np.abs(np.dot(matrix[:dim, :dim], matrix[:dim, :dim].T)
                   - _IDENTITY[:dim, :dim]).max()
    # if the matrix differs by more than float-zero and less
    # than the threshold try to repair the matrix with SVD
    if check > 1e-13 and check < max_deviance:
        # reconstruct the rotation from the SVD
        U, _, V = np.linalg.svd(matrix[:dim, :dim])
        repaired = np.eye(dim + 1)
        repaired[:dim, :dim] = np.dot(U, V)
        # copy in the translation
        repaired[:dim, dim] = matrix[:dim, dim]
        # should be within tolerance of the original matrix
        assert np.allclose(repaired, matrix, atol=max_deviance)
        return repaired

    return matrix


def is_rigid(matrix, epsilon=1e-8):
    """
    Check to make sure a homogeonous transformation
    matrix is a rigid transform.

    Parameters
    -----------
    matrix : (4, 4) float
      A transformation matrix

    Returns
    -----------
    check : bool
      True if matrix is a a transform with
      only translation, scale, and rotation
    """

    matrix = np.asanyarray(matrix, dtype=np.float64)

    if matrix.shape != (4, 4):
        return False

    # make sure last row has no scaling
    if (matrix[-1] - [0, 0, 0, 1]).ptp() > epsilon:
        return False

    # check dot product of rotation against transpose
    check = np.dot(matrix[:3, :3],
                   matrix[:3, :3].T) - _IDENTITY[:3, :3]

    return check.ptp() < epsilon


def scale_and_translate(scale=None, translate=None):
    """
    Optimized version of `compose_matrix` for just
    scaling then translating.

    Scalar args are broadcast to arrays of shape (3,)

    Parameters
    --------------
    scale : float or (3,) float
      Scale factor
    translate : float or (3,) float
      Translation
    """
    M = np.eye(4)
    if np.any(scale != 1):
        M[:3, :3] *= scale
    if translate is not None:
        M[:3, 3] = translate
    return M


def flips_winding(matrix):
    """
    Check to see if a matrix will invert triangles.

    Parameters
    -------------
    matrix : (4, 4) float
      Homogeneous transformation matrix

    Returns
    --------------
    flip : bool
      True if matrix will flip winding of triangles.
    """
    # get input as numpy array
    matrix = np.asanyarray(matrix, dtype=np.float64)
    # how many random triangles do we really want
    count = 3
    # test rotation against some random triangles
    tri = np.random.random((count * 3, 3))
    rot = np.dot(matrix[:3, :3], tri.T).T

    # stack them into one triangle soup
    triangles = np.vstack((tri, rot)).reshape((-1, 3, 3))
    # find the normals of every triangle
    vectors = np.diff(triangles, axis=1)
    cross = np.cross(vectors[:, 0], vectors[:, 1])
    # rotate the original normals to match
    cross[:count] = np.dot(matrix[:3, :3],
                           cross[:count].T).T
    # unitize normals
    norm = np.sqrt(np.dot(cross * cross, [1, 1, 1])).reshape((-1, 1))
    cross = cross / norm
    # find the projection of the two normals
    projection = np.dot(cross[:count] * cross[count:],
                        [1.0] * 3)
    # if the winding was flipped but not the normal
    # the projection will be negative, and since we're
    # checking a few triangles check against the mean
    flip = projection.mean() < 0.0

    return flip


# trimesh/triangles.py

"""
triangles.py
-------------

Functions for dealing with triangle soups in (n, 3, 3) float form.
"""
import numpy as np

from . import util

from .util import unitize, diagonal_dot
from .points import point_plane_distance
from .constants import tol


def cross(triangles):
    """
    Returns the cross product of two edges from input triangles

    Parameters
    --------------
    triangles: (n, 3, 3) float
      Vertices of triangles

    Returns
    --------------
    crosses : (n, 3) float
      Cross product of two edge vectors
    """
    vectors = np.diff(triangles, axis=1)
    crosses = np.cross(vectors[:, 0], vectors[:, 1])
    return crosses


def area(triangles=None, crosses=None, sum=False):
    """
    Calculates the sum area of input triangles

    Parameters
    ----------
    triangles : (n, 3, 3) float
      Vertices of triangles
    crosses : (n, 3) float or None
      As a speedup don't re- compute cross products
    sum : bool
      Return summed area or individual triangle area

    Returns
    ----------
    area : (n,) float or float
      Individual or summed area depending on `sum` argument
    """
    if crosses is None:
        crosses = cross(triangles)
    areas = np.sqrt((crosses ** 2).sum(axis=1)) / 2.0
    if sum:
        return areas.sum()
    return areas


def normals(triangles=None, crosses=None):
    """
    Calculates the normals of input triangles

    Parameters
    ------------
    triangles : (n, 3, 3) float
      Vertex positions
    crosses : (n, 3) float
      Cross products of edge vectors

    Returns
    ------------
    normals : (m, 3) float
      Normal vectors
    valid : (n,) bool
      Was the face nonzero area or not
    """
    if crosses is None:
        crosses = cross(triangles)
    # unitize the cross product vectors
    unit, valid = unitize(crosses, check_valid=True)
    return unit, valid


def angles(triangles):
    """
    Calculates the angles of input triangles.

    Parameters
    ------------
    triangles : (n, 3, 3) float
      Vertex positions

    Returns
    ------------
    angles : (n, 3) float
      Angles at vertex positions in radians
      Degenerate angles will be returned as zero
    """
    # don't copy triangles
    triangles = np.asanyarray(triangles, dtype=np.float64)

    # get a unit vector for each edge of the triangle
    u = unitize(triangles[:, 1] - triangles[:, 0])
    v = unitize(triangles[:, 2] - triangles[:, 0])
    w = unitize(triangles[:, 2] - triangles[:, 1])

    # run the cosine and per-row dot product
    result = np.zeros((len(triangles), 3), dtype=np.float64)
    # clip to make sure we don't float error past 1.0
    result[:, 0] = np.arccos(np.clip(diagonal_dot(u, v), -1, 1))
    result[:, 1] = np.arccos(np.clip(diagonal_dot(-u, w), -1, 1))
    # the third angle is just the remaining
    result[:, 2] = np.pi - result[:, 0] - result[:, 1]

    # a triangle with any zero angles is degenerate
    # so set all of the angles to zero in that case
    result[(result < tol.merge).any(axis=1), :] = 0.0

    return result


def all_coplanar(triangles):
    """
    Check to see if a list of triangles are all coplanar

    Parameters
    ----------------
    triangles: (n, 3, 3) float
      Vertices of triangles

    Returns
    ---------------
    all_coplanar : bool
      True if all triangles are coplanar
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')

    test_normal = normals(triangles)[0]
    test_vertex = triangles[0][0]
    distances = point_plane_distance(points=triangles[1:].reshape((-1, 3)),
                                     plane_normal=test_normal,
                                     plane_origin=test_vertex)
    all_coplanar = np.all(np.abs(distances) < tol.zero)
    return all_coplanar


def any_coplanar(triangles):
    """
    For a list of triangles if the FIRST triangle is coplanar
    with ANY of the following triangles, return True.
    Otherwise, return False.
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')

    test_normal = normals(triangles)[0]
    test_vertex = triangles[0][0]
    distances = point_plane_distance(points=triangles[1:].reshape((-1, 3)),
                                     plane_normal=test_normal,
                                     plane_origin=test_vertex)
    any_coplanar = np.any(
        np.all(np.abs(distances.reshape((-1, 3)) < tol.zero), axis=1))
    return any_coplanar


def mass_properties(triangles,
                    crosses=None,
                    density=1.0,
                    center_mass=None,
                    skip_inertia=False):
    """
    Calculate the mass properties of a group of triangles.

    Implemented from:
    http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf

    Parameters
    ----------
    triangles : (n, 3, 3) float
      Triangle vertices in space
    crosses : (n,) float
      Optional cross products of triangles
    density : float
      Optional override for density
    center_mass :  (3,) float
      Optional override for center mass
    skip_inertia : bool
      if True will not return moments matrix

    Returns
    ---------
    info : dict
      Mass properties
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')

    if crosses is None:
        crosses = cross(triangles)

    # these are the subexpressions of the integral
    # this is equvilant but 7x faster than triangles.sum(axis=1)
    f1 = triangles[:, 0, :] + triangles[:, 1, :] + triangles[:, 2, :]

    # for the the first vertex of every triangle:
    # triangles[:,0,:] will give rows like [[x0, y0, z0], ...]

    # for the x coordinates of every triangle
    # triangles[:,:,0] will give rows like [[x0, x1, x2], ...]
    f2 = (triangles[:, 0, :]**2 +
          triangles[:, 1, :]**2 +
          triangles[:, 0, :] * triangles[:, 1, :] +
          triangles[:, 2, :] * f1)
    f3 = ((triangles[:, 0, :]**3) +
          (triangles[:, 0, :]**2) * (triangles[:, 1, :]) +
          (triangles[:, 0, :]) * (triangles[:, 1, :]**2) +
          (triangles[:, 1, :]**3) +
          (triangles[:, 2, :] * f2))
    g0 = (f2 + (triangles[:, 0, :] + f1) * triangles[:, 0, :])
    g1 = (f2 + (triangles[:, 1, :] + f1) * triangles[:, 1, :])
    g2 = (f2 + (triangles[:, 2, :] + f1) * triangles[:, 2, :])
    integral = np.zeros((10, len(f1)))
    integral[0] = crosses[:, 0] * f1[:, 0]
    integral[1:4] = (crosses * f2).T
    integral[4:7] = (crosses * f3).T
    for i in range(3):
        triangle_i = np.mod(i + 1, 3)
        integral[i + 7] = crosses[:, i] * (
            (triangles[:, 0, triangle_i] * g0[:, i]) +
            (triangles[:, 1, triangle_i] * g1[:, i]) +
            (triangles[:, 2, triangle_i] * g2[:, i]))

    coefficients = 1.0 / np.array(
        [6, 24, 24, 24, 60, 60, 60, 120, 120, 120],
        dtype=np.float64)
    integrated = integral.sum(axis=1) * coefficients

    volume = integrated[0]

    if center_mass is None:
        if np.abs(volume) < tol.zero:
            center_mass = np.zeros(3)
        else:
            center_mass = integrated[1:4] / volume

    mass = density * volume

    result = {'density': density,
              'mass': mass,
              'volume': volume,
              'center_mass': center_mass}

    if skip_inertia:
        return result

    inertia = np.zeros((3, 3))
    inertia[0, 0] = integrated[5] + integrated[6] - \
        (volume * (center_mass[[1, 2]]**2).sum())
    inertia[1, 1] = integrated[4] + integrated[6] - \
        (volume * (center_mass[[0, 2]]**2).sum())
    inertia[2, 2] = integrated[4] + integrated[5] - \
        (volume * (center_mass[[0, 1]]**2).sum())
    inertia[0, 1] = - (
        integrated[7] - (volume * np.prod(center_mass[[0, 1]])))
    inertia[1, 2] = - (
        integrated[8] - (volume * np.prod(center_mass[[1, 2]])))
    inertia[0, 2] = - (
        integrated[9] - (volume * np.prod(center_mass[[0, 2]])))
    inertia[2, 0] = inertia[0, 2]
    inertia[2, 1] = inertia[1, 2]
    inertia[1, 0] = inertia[0, 1]
    inertia *= density
    result['inertia'] = inertia

    return result


def windings_aligned(triangles, normals_compare):
    """
    Given a list of triangles and a list of normals determine if the
    two are aligned

    Parameters
    ----------
    triangles : (n, 3, 3) float
      Vertex locations in space
    normals_compare : (n, 3) float
      List of normals to compare

    Returns
    ----------
    aligned : (n,) bool
      Are normals aligned with triangles
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3), allow_zeros=True):
        raise ValueError(
            'triangles must have shape (n, 3, 3), got %s' % str(triangles.shape))
    normals_compare = np.asanyarray(normals_compare, dtype=np.float64)

    calculated, valid = normals(triangles)
    if normals_compare.shape == (3,):
        # single comparison vector case
        difference = np.dot(calculated, normals_compare)
    else:
        # multiple comparison case
        difference = diagonal_dot(
            calculated, normals_compare[valid])

    aligned = np.zeros(len(triangles), dtype=bool)
    aligned[valid] = difference > 0.0

    return aligned


def bounds_tree(triangles):
    """
    Given a list of triangles, create an r-tree for broad- phase
    collision detection

    Parameters
    ---------
    triangles : (n, 3, 3) float
      Triangles in space

    Returns
    ---------
    tree : rtree.Rtree
      One node per triangle
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')

    # the (n,6) interleaved bounding box for every triangle
    triangle_bounds = np.column_stack((triangles.min(axis=1),
                                       triangles.max(axis=1)))
    tree = util.bounds_tree(triangle_bounds)
    return tree


def nondegenerate(triangles, areas=None, height=None):
    """
    Find all triangles which have an oriented bounding box
    where both of the two sides is larger than a specified height.

    Degenerate triangles can be when:
    1) Two of the three vertices are colocated
    2) All three vertices are unique but colinear


    Parameters
    ----------
    triangles : (n, 3, 3) float
      Triangles in space
    height : float
      Minimum edge length of a triangle to keep

    Returns
    ----------
    nondegenerate : (n,) bool
      True if a triangle meets required minimum height
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')

    if height is None:
        height = tol.merge

    # if both edges of the triangles OBB are longer than tol.merge
    # we declare them to be nondegenerate
    ok = (extents(triangles=triangles,
                  areas=areas) > height).all(axis=1)

    return ok


def extents(triangles, areas=None):
    """
    Return the 2D bounding box size of each triangle.

    Parameters
    ----------
    triangles : (n, 3, 3) float
      Triangles in space
    areas : (n,) float
      Optional area of input triangles

    Returns
    ----------
    box :  (n, 2) float
      The size of each triangle's 2D oriented bounding box
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')

    if areas is None:
        areas = area(triangles=triangles,
                     sum=False)

    # the edge vectors which define the triangle
    a = triangles[:, 1] - triangles[:, 0]
    b = triangles[:, 2] - triangles[:, 0]

    # length of the edge vectors
    length_a = (a**2).sum(axis=1)**.5
    length_b = (b**2).sum(axis=1)**.5

    # which edges are acceptable length
    nonzero_a = length_a > tol.merge
    nonzero_b = length_b > tol.merge

    # find the two heights of the triangle
    # essentially this is the side length of an
    # oriented bounding box, per triangle
    box = np.zeros((len(triangles), 2), dtype=np.float64)
    box[:, 0][nonzero_a] = (areas[nonzero_a] * 2) / length_a[nonzero_a]
    box[:, 1][nonzero_b] = (areas[nonzero_b] * 2) / length_b[nonzero_b]

    return box


def barycentric_to_points(triangles, barycentric):
    """
    Convert a list of barycentric coordinates on a list of triangles
    to cartesian points.

    Parameters
    ------------
    triangles : (n, 3, 3) float
      Triangles in space
    barycentric : (n, 2) float
      Barycentric coordinates

    Returns
    -----------
    points : (m, 3) float
      Points in space
    """
    barycentric = np.asanyarray(barycentric, dtype=np.float64)
    triangles = np.asanyarray(triangles, dtype=np.float64)

    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')
    if barycentric.shape == (2,):
        barycentric = np.ones((len(triangles), 2),
                              dtype=np.float64) * barycentric
    if util.is_shape(barycentric, (len(triangles), 2)):
        barycentric = np.column_stack((barycentric,
                                       1.0 - barycentric.sum(axis=1)))
    elif not util.is_shape(barycentric, (len(triangles), 3)):
        raise ValueError('Barycentric shape incorrect!')

    barycentric /= barycentric.sum(axis=1).reshape((-1, 1))
    points = (triangles * barycentric.reshape((-1, 3, 1))).sum(axis=1)

    return points


def points_to_barycentric(triangles,
                          points,
                          method='cramer'):
    """
    Find the barycentric coordinates of points relative to triangles.

    The Cramer's rule solution implements:
        http://blackpawn.com/texts/pointinpoly

    The cross product solution implements:
        https://www.cs.ubc.ca/~heidrich/Papers/JGT.05.pdf


    Parameters
    -----------
    triangles : (n, 3, 3) float
      Triangles vertices in space
    points : (n, 3) float
      Point in space associated with a triangle
    method :  str
      Which method to compute the barycentric coordinates with:
        - 'cross': uses a method using cross products, roughly 2x slower but
                  different numerical robustness properties
        - anything else: uses a cramer's rule solution

    Returns
    -----------
    barycentric : (n, 3) float
      Barycentric coordinates of each point
    """

    def method_cross():
        n = np.cross(edge_vectors[:, 0], edge_vectors[:, 1])
        denominator = diagonal_dot(n, n)

        barycentric = np.zeros((len(triangles), 3), dtype=np.float64)
        barycentric[:, 2] = diagonal_dot(
            np.cross(edge_vectors[:, 0], w), n) / denominator
        barycentric[:, 1] = diagonal_dot(
            np.cross(w, edge_vectors[:, 1]), n) / denominator
        barycentric[:, 0] = 1 - barycentric[:, 1] - barycentric[:, 2]
        return barycentric

    def method_cramer():
        dot00 = diagonal_dot(edge_vectors[:, 0], edge_vectors[:, 0])
        dot01 = diagonal_dot(edge_vectors[:, 0], edge_vectors[:, 1])
        dot02 = diagonal_dot(edge_vectors[:, 0], w)
        dot11 = diagonal_dot(edge_vectors[:, 1], edge_vectors[:, 1])
        dot12 = diagonal_dot(edge_vectors[:, 1], w)

        inverse_denominator = 1.0 / (dot00 * dot11 - dot01 * dot01)

        barycentric = np.zeros((len(triangles), 3), dtype=np.float64)
        barycentric[:, 2] = (dot00 * dot12 - dot01 *
                             dot02) * inverse_denominator
        barycentric[:, 1] = (dot11 * dot02 - dot01 *
                             dot12) * inverse_denominator
        barycentric[:, 0] = 1 - barycentric[:, 1] - barycentric[:, 2]
        return barycentric

    # establish that input triangles and points are sane
    triangles = np.asanyarray(triangles, dtype=np.float64)
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('triangles shape incorrect')
    if not util.is_shape(points, (len(triangles), 3)):
        raise ValueError('triangles and points must correspond')

    edge_vectors = triangles[:, 1:] - triangles[:, :1]
    w = points - triangles[:, 0].reshape((-1, 3))

    if method == 'cross':
        return method_cross()
    return method_cramer()


def closest_point(triangles, points):
    """
    Return the closest point on the surface of each triangle for a
    list of corresponding points.

    Implements the method from "Real Time Collision Detection" and
    use the same variable names as "ClosestPtPointTriangle" to avoid
    being any more confusing.


    Parameters
    ----------
    triangles : (n, 3, 3) float
      Triangle vertices in space
    points : (n, 3) float
      Points in space

    Returns
    ----------
    closest : (n, 3) float
      Point on each triangle closest to each point
    """

    # check input triangles and points
    triangles = np.asanyarray(triangles, dtype=np.float64)
    points = np.asanyarray(points, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('triangles shape incorrect')
    if not util.is_shape(points, (len(triangles), 3)):
        raise ValueError('need same number of triangles and points!')

    # store the location of the closest point
    result = np.zeros_like(points)
    # which points still need to be handled
    remain = np.ones(len(points), dtype=bool)

    # if we dot product this against a (n, 3)
    # it is equivalent but faster than array.sum(axis=1)
    ones = [1.0, 1.0, 1.0]

    # get the three points of each triangle
    # use the same notation as RTCD to avoid confusion
    a = triangles[:, 0, :]
    b = triangles[:, 1, :]
    c = triangles[:, 2, :]

    # check if P is in vertex region outside A
    ab = b - a
    ac = c - a
    ap = points - a
    # this is a faster equivalent of:
    # diagonal_dot(ab, ap)
    d1 = np.dot(ab * ap, ones)
    d2 = np.dot(ac * ap, ones)

    # is the point at A
    is_a = np.logical_and(d1 < tol.zero, d2 < tol.zero)
    if any(is_a):
        result[is_a] = a[is_a]
        remain[is_a] = False

    # check if P in vertex region outside B
    bp = points - b
    d3 = np.dot(ab * bp, ones)
    d4 = np.dot(ac * bp, ones)

    # do the logic check
    is_b = (d3 > -tol.zero) & (d4 <= d3) & remain
    if any(is_b):
        result[is_b] = b[is_b]
        remain[is_b] = False

    # check if P in edge region of AB, if so return projection of P onto A
    vc = (d1 * d4) - (d3 * d2)
    is_ab = ((vc < tol.zero) &
             (d1 > -tol.zero) &
             (d3 < tol.zero) & remain)
    if any(is_ab):
        v = (d1[is_ab] / (d1[is_ab] - d3[is_ab])).reshape((-1, 1))
        result[is_ab] = a[is_ab] + (v * ab[is_ab])
        remain[is_ab] = False

    # check if P in vertex region outside C
    cp = points - c
    d5 = np.dot(ab * cp, ones)
    d6 = np.dot(ac * cp, ones)
    is_c = (d6 > -tol.zero) & (d5 <= d6) & remain
    if any(is_c):
        result[is_c] = c[is_c]
        remain[is_c] = False

    # check if P in edge region of AC, if so return projection of P onto AC
    vb = (d5 * d2) - (d1 * d6)
    is_ac = (vb < tol.zero) & (d2 > -tol.zero) & (d6 < tol.zero) & remain
    if any(is_ac):
        w = (d2[is_ac] / (d2[is_ac] - d6[is_ac])).reshape((-1, 1))
        result[is_ac] = a[is_ac] + w * ac[is_ac]
        remain[is_ac] = False

    # check if P in edge region of BC, if so return projection of P onto BC
    va = (d3 * d6) - (d5 * d4)
    is_bc = ((va < tol.zero) &
             ((d4 - d3) > - tol.zero) &
             ((d5 - d6) > -tol.zero) & remain)
    if any(is_bc):
        d43 = d4[is_bc] - d3[is_bc]
        w = (d43 / (d43 + (d5[is_bc] - d6[is_bc]))).reshape((-1, 1))
        result[is_bc] = b[is_bc] + w * (c[is_bc] - b[is_bc])
        remain[is_bc] = False

    # any remaining points must be inside face region
    if any(remain):
        # point is inside face region
        denom = 1.0 / (va[remain] + vb[remain] + vc[remain])
        v = (vb[remain] * denom).reshape((-1, 1))
        w = (vc[remain] * denom).reshape((-1, 1))
        # compute Q through its barycentric coordinates
        result[remain] = a[remain] + (ab[remain] * v) + (ac[remain] * w)

    return result


def to_kwargs(triangles):
    """
    Convert a list of triangles to the kwargs for the Trimesh
    constructor.

    Parameters
    ---------
    triangles : (n, 3, 3) float
      Triangles in space

    Returns
    ---------
    kwargs : dict
      Keyword arguments for the trimesh.Trimesh constructor
      Includes keys 'vertices' and 'faces'

    Examples
    ---------
    >>> mesh = trimesh.Trimesh(**trimesh.triangles.to_kwargs(triangles))
    """
    triangles = np.asanyarray(triangles, dtype=np.float64)
    if not util.is_shape(triangles, (-1, 3, 3)):
        raise ValueError('Triangles must be (n, 3, 3)!')

    vertices = triangles.reshape((-1, 3))
    faces = np.arange(len(vertices)).reshape((-1, 3))
    kwargs = {'vertices': vertices,
              'faces': faces}

    return kwargs


# trimesh/units.py

"""
units.py
--------------
Deal with physical unit systems (i.e. inches, mm)

Very basic conversions, and no requirement for
sympy.physics.units or pint.
"""
from .constants import log
from . import resources


def unit_conversion(current, desired):
    """
    Calculate the conversion from one set of units to another.

    Parameters
    ---------
    current : str
        Unit system values are in now (eg 'millimeters')
    desired : str
        Unit system we'd like values in (eg 'inches')

    Returns
    ---------
    conversion : float
        Number to multiply by to put values into desired units
    """
    # scaling factors from various unit systems to inches
    to_inch = resources.get(
        'units_to_inches.json', decode_json=True)

    current = str(current).strip().lower()
    desired = str(desired).strip().lower()
    conversion = to_inch[current] / to_inch[desired]
    return conversion


def units_from_metadata(obj, guess=True):
    """
    Try to extract hints from metadata and if that fails
    guess based on the object scale.


    Parameters
    ------------
    obj: object
        Has attributes 'metadata' (dict) and 'scale' (float)
    guess : bool
        If metadata doesn't indicate units, guess from scale

    Returns
    ------------
    units: str
        A guess of what the units might be
    """
    to_inch = resources.get(
        'units_to_inches.json', decode_json=True)

    # try to guess from metadata
    for key in ['file_name', 'name']:
        if key not in obj.metadata:
            continue
        # get the string which might contain unit hints
        hints = obj.metadata[key].lower()
        if 'unit' in hints:
            # replace all delimiter options with white space
            for delim in '_-.':
                hints = hints.replace(delim, ' ')
            # loop through each hint
            for hint in hints.strip().split():
                # key word is "unit" or "units"
                if 'unit' not in hint:
                    continue
                # get rid of keyword and whitespace
                hint = hint.replace(
                    'units', '').replace(
                        'unit', '').strip()
                # if the hint is a valid unit return it
                if hint in to_inch:
                    return hint

    if not guess:
        raise ValueError('no units and not allowed to guess')

    # we made it to the wild ass guess section
    # if the scale is larger than 100 mystery units
    # declare the model to be millimeters, otherwise inches
    log.debug('no units: guessing from scale')
    if float(obj.scale) > 100.0:
        return 'millimeters'
    else:
        return 'inches'


def _convert_units(obj, desired, guess=False):
    """
    Given an object with scale and units try to scale
    to different units via the object's `apply_scale`.

    Parameters
    ---------
    obj :  object
        With apply_scale method (i.e. Trimesh, Path2D, etc)
    desired : str
        Units desired (eg 'inches')
    guess:   bool
        Whether we are allowed to guess the units
        if they are not specified.
    """
    if obj.units is None:
        # try to extract units from metadata
        # if nothing specified in metadata and not allowed
        # to guess will raise a ValueError
        obj.units = units_from_metadata(obj, guess=guess)

    log.debug('converting units from %s to %s', obj.units, desired)
    # float, conversion factor
    conversion = unit_conversion(obj.units, desired)

    # apply scale uses transforms which preserve
    # cached properties rather than just multiplying vertices
    obj.apply_scale(conversion)
    # units are now desired units
    obj.units = desired


# trimesh/util.py

"""
util.py
-----------

Standalone functions which require only imports from numpy and the
standard library.

Other libraries may be imported must be wrapped in try/except blocks
or imported inside of a function
"""

import abc
import sys
import copy
import json
import uuid
import base64
import random
import shutil
import logging
import zipfile
import tempfile
import collections

import numpy as np

if sys.version_info >= (3, 4):
    # for newer version of python
    ABC = abc.ABC
else:
    # an abstract base class that works on older versions
    ABC = abc.ABCMeta('ABC', (), {})

# a flag we can check elsewhere for Python 3
PY3 = sys.version_info.major >= 3

if PY3:
    # for type checking
    basestring = str
    # Python 3
    from io import BytesIO, StringIO
    from shutil import which  # noqa
    from time import perf_counter as now  # noqa
else:
    # Python 2
    from StringIO import StringIO
    from distutils.spawn import find_executable as which  # noqa
    # monkey patch StringIO so `with` statements work
    StringIO.__enter__ = lambda a: a
    StringIO.__exit__ = lambda a, b, c, d: a.close()
    BytesIO = StringIO
    from time import time as now  # noqa


try:
    from collections.abc import Mapping
except ImportError:
    from collections import Mapping

# create a default logger
log = logging.getLogger('trimesh')

# include constants here so we don't have to import
# a floating point threshold for 0.0
# we are setting it to 100x the resolution of a float64
# which works out to be 1e-13
TOL_ZERO = np.finfo(np.float64).resolution * 100
# how close to merge vertices
TOL_MERGE = 1e-8
# enable additional potentially slow checks
_STRICT = False

_IDENTITY = np.eye(4, dtype=np.float64)
_IDENTITY.flags['WRITEABLE'] = False


def has_module(name):
    """
    Check to see if a module is installed by name without
    actually importing the module.

    Parameters
    ------------
    name : str
      The name of the module to check

    Returns
    ------------
    installed : bool
      True if module is installed
    """
    # this should work on Python 2.7 and 3.4+
    import pkgutil
    return pkgutil.find_loader(name) is not None


def unitize(vectors,
            check_valid=False,
            threshold=None):
    """
    Unitize a vector or an array or row-vectors.

    Parameters
    ------------
    vectors : (n,m) or (j) float
       Vector or vectors to be unitized
    check_valid :  bool
       If set, will return mask of nonzero vectors
    threshold : float
       Cutoff for a value to be considered zero.

    Returns
    ---------
    unit :  (n,m) or (j) float
       Input vectors but unitized
    valid : (n,) bool or bool
        Mask of nonzero vectors returned if `check_valid`
    """
    # make sure we have a numpy array
    vectors = np.asanyarray(vectors)

    # allow user to set zero threshold
    if threshold is None:
        threshold = TOL_ZERO

    if len(vectors.shape) == 2:
        # for (m, d) arrays take the per-row unit vector
        # using sqrt and avoiding exponents is slightly faster
        # also dot with ones is faser than .sum(axis=1)
        norm = np.sqrt(np.dot(vectors * vectors,
                              [1.0] * vectors.shape[1]))
        # non-zero norms
        valid = norm > threshold
        # in-place reciprocal of nonzero norms
        norm[valid] **= -1
        # multiply by reciprocal of norm
        unit = vectors * norm.reshape((-1, 1))

    elif len(vectors.shape) == 1:
        # treat 1D arrays as a single vector
        norm = np.sqrt(np.dot(vectors, vectors))
        valid = norm > threshold
        if valid:
            unit = vectors / norm
        else:
            unit = vectors.copy()
    else:
        raise ValueError('vectors must be (n, ) or (n, d)!')

    if check_valid:
        return unit[valid], valid
    return unit


def euclidean(a, b):
    """
    Euclidean distance between vectors a and b.

    Parameters
    ------------
    a : (n,) float
       First vector
    b : (n,) float
       Second vector

    Returns
    ------------
    distance : float
        Euclidean distance between A and B
    """
    a = np.asanyarray(a, dtype=np.float64)
    b = np.asanyarray(b, dtype=np.float64)
    return np.sqrt(((a - b) ** 2).sum())


def is_file(obj):
    """
    Check if an object is file-like

    Parameters
    ------------
    obj : object
       Any object type to be checked

    Returns
    -----------
    is_file : bool
        True if object is a file
    """
    return hasattr(obj, 'read') or hasattr(obj, 'write')


def is_pathlib(obj):
    """
    Check if the object is a `pathlib.Path` or subclass.

    Parameters
    ------------
    obj : object
      Object to be checked

    Returns
    ------------
    is_pathlib : bool
      Is the input object a pathlib path
    """
    # check class name rather than a pathlib import
    name = obj.__class__.__name__
    return hasattr(obj, 'absolute') and name.endswith('Path')


def is_string(obj):
    """
    Check if an object is a string.

    Parameters
    ------------
    obj : object
       Any object type to be checked

    Returns
    ------------
    is_string : bool
        True if obj is a string
    """
    return isinstance(obj, basestring)


def is_none(obj):
    """
    Check to see if an object is None or not.

    Handles the case of np.array(None) as well.

    Parameters
    -------------
    obj : object
      Any object type to be checked

    Returns
    -------------
    is_none : bool
        True if obj is None or numpy None-like
    """
    if obj is None:
        return True
    if (is_sequence(obj) and
        len(obj) == 1 and
            obj[0] is None):
        return True
    return False


def is_sequence(obj):
    """
    Check if an object is a sequence or not.

    Parameters
    -------------
    obj : object
      Any object type to be checked

    Returns
    -------------
    is_sequence : bool
        True if object is sequence
    """
    seq = (not hasattr(obj, "strip") and
           hasattr(obj, "__getitem__") or
           hasattr(obj, "__iter__"))

    # check to make sure it is not a set, string, or dictionary
    seq = seq and all(not isinstance(obj, i) for i in (dict,
                                                       set,
                                                       basestring))

    # PointCloud objects can look like an array but are not
    seq = seq and type(obj).__name__ not in ['PointCloud']

    # numpy sometimes returns objects that are single float64 values
    # but sure look like sequences, so we check the shape
    if hasattr(obj, 'shape'):
        seq = seq and obj.shape != ()

    return seq


def is_shape(obj, shape, allow_zeros=False):
    """
    Compare the shape of a numpy.ndarray to a target shape,
    with any value less than zero being considered a wildcard

    Note that if a list-like object is passed that is not a numpy
    array, this function will not convert it and will return False.

    Parameters
    ------------
    obj :   np.ndarray
      Array to check the shape on
    shape : list or tuple
      Any negative term will be considered a wildcard
      Any tuple term will be evaluated as an OR
    allow_zeros: bool
      if False, zeros do not match negatives in shape

    Returns
    ---------
    shape_ok : bool
      True if shape of obj matches query shape

    Examples
    ------------------------
    In [1]: a = np.random.random((100, 3))

    In [2]: a.shape
    Out[2]: (100, 3)

    In [3]: trimesh.util.is_shape(a, (-1, 3))
    Out[3]: True

    In [4]: trimesh.util.is_shape(a, (-1, 3, 5))
    Out[4]: False

    In [5]: trimesh.util.is_shape(a, (100, -1))
    Out[5]: True

    In [6]: trimesh.util.is_shape(a, (-1, (3, 4)))
    Out[6]: True

    In [7]: trimesh.util.is_shape(a, (-1, (4, 5)))
    Out[7]: False
    """

    # if the obj.shape is different length than
    # the goal shape it means they have different number
    # of dimensions and thus the obj is not the query shape
    if (not hasattr(obj, 'shape') or
            len(obj.shape) != len(shape)):
        return False

    # empty lists with any flexible dimensions match
    if len(obj) == 0 and -1 in shape:
        return True

    # loop through each integer of the two shapes
    # multiple values are sequences
    # wildcards are less than zero (i.e. -1)
    for i, target in zip(obj.shape, shape):
        # check if current field has multiple acceptable values
        if is_sequence(target):
            if i in target:
                # obj shape is in the accepted values
                continue
            else:
                return False

        # check if current field is a wildcard
        if target < 0:
            if i == 0 and not allow_zeros:
                # if a dimension is 0, we don't allow
                # that to match to a wildcard
                # it would have to be explicitly called out as 0
                return False
            else:
                continue
        # since we have a single target and a single value,
        # if they are not equal we have an answer
        if target != i:
            return False

    # since none of the checks failed the obj.shape
    # matches the pattern
    return True


def make_sequence(obj):
    """
    Given an object, if it is a sequence return, otherwise
    add it to a length 1 sequence and return.

    Useful for wrapping functions which sometimes return single
    objects and other times return lists of objects.

    Parameters
    -------------
    obj : object
      An object to be made a sequence

    Returns
    --------------
    as_sequence : (n,) sequence
       Contains input value
    """
    if is_sequence(obj):
        return np.array(list(obj))
    else:
        return np.array([obj])


def vector_hemisphere(vectors, return_sign=False):
    """
    For a set of 3D vectors alter the sign so they are all in the
    upper hemisphere.

    If the vector lies on the plane all vectors with negative Y
    will be reversed.

    If the vector has a zero Z and Y value vectors with a
    negative X value will be reversed.

    Parameters
    ------------
    vectors : (n, 3) float
      Input vectors
    return_sign : bool
      Return the sign mask or not

    Returns
    ----------
    oriented: (n, 3) float
      Vectors with same magnitude as source
      but possibly reversed to ensure all vectors
      are in the same hemisphere.
    sign : (n,) float
      [OPTIONAL] sign of original vectors
    """
    # vectors as numpy array
    vectors = np.asanyarray(vectors, dtype=np.float64)

    if is_shape(vectors, (-1, 2)):
        # 2D vector case
        # check the Y value and reverse vector
        # direction if negative.
        negative = vectors < -TOL_ZERO
        zero = np.logical_not(
            np.logical_or(negative, vectors > TOL_ZERO))

        signs = np.ones(len(vectors), dtype=np.float64)
        # negative Y values are reversed
        signs[negative[:, 1]] = -1.0

        # zero Y and negative X are reversed
        signs[np.logical_and(zero[:, 1], negative[:, 0])] = -1.0

    elif is_shape(vectors, (-1, 3)):
        # 3D vector case
        negative = vectors < -TOL_ZERO
        zero = np.logical_not(
            np.logical_or(negative, vectors > TOL_ZERO))
        # move all                          negative Z to positive
        # then for zero Z vectors, move all negative Y to positive
        # then for zero Y vectors, move all negative X to positive
        signs = np.ones(len(vectors), dtype=np.float64)
        # all vectors with negative Z values
        signs[negative[:, 2]] = -1.0
        # all on-plane vectors with negative Y values
        signs[np.logical_and(zero[:, 2], negative[:, 1])] = -1.0
        # all on-plane vectors with zero Y values
        # and negative X values
        signs[np.logical_and(np.logical_and(zero[:, 2],
                                            zero[:, 1]),
                             negative[:, 0])] = -1.0

    else:
        raise ValueError('vectors must be (n, 3)!')

    # apply the signs to the vectors
    oriented = vectors * signs.reshape((-1, 1))

    if return_sign:
        return oriented, signs

    return oriented


def vector_to_spherical(cartesian):
    """
    Convert a set of cartesian points to (n, 2) spherical unit
    vectors.

    Parameters
    ------------
    cartesian : (n, 3) float
       Points in space

    Returns
    ------------
    spherical : (n, 2) float
       Angles, in radians
    """
    cartesian = np.asanyarray(cartesian, dtype=np.float64)
    if not is_shape(cartesian, (-1, 3)):
        raise ValueError('Cartesian points must be (n, 3)!')

    unit, valid = unitize(cartesian, check_valid=True)
    unit[np.abs(unit) < TOL_MERGE] = 0.0

    x, y, z = unit.T
    spherical = np.zeros((len(cartesian), 2), dtype=np.float64)
    spherical[valid] = np.column_stack((np.arctan2(y, x),
                                        np.arccos(z)))
    return spherical


def spherical_to_vector(spherical):
    """
    Convert a set of (n, 2) spherical vectors to (n, 3) vectors

    Parameters
    ------------
    spherical : (n , 2) float
       Angles, in radians

    Returns
    -----------
    vectors : (n, 3) float
      Unit vectors
    """
    spherical = np.asanyarray(spherical, dtype=np.float64)
    if not is_shape(spherical, (-1, 2)):
        raise ValueError('spherical coordinates must be (n, 2)!')

    theta, phi = spherical.T
    st, ct = np.sin(theta), np.cos(theta)
    sp, cp = np.sin(phi), np.cos(phi)
    vectors = np.column_stack((ct * sp,
                               st * sp,
                               cp))
    return vectors


def pairwise(iterable):
    """
    For an iterable, group values into pairs.

    Parameters
    ------------
    iterable : (m, ) list
       A sequence of values

    Returns
    -----------
    pairs: (n, 2)
      Pairs of sequential values

    Example
    -----------
    In [1]: data
    Out[1]: [0, 1, 2, 3, 4, 5, 6]

    In [2]: list(trimesh.util.pairwise(data))
    Out[2]: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]

    """
    # looping through a giant numpy array would be dumb
    # so special case ndarrays and use numpy operations
    if isinstance(iterable, np.ndarray):
        iterable = iterable.reshape(-1)
        stacked = np.column_stack((iterable, iterable))
        pairs = stacked.reshape(-1)[1:-1].reshape((-1, 2))
        return pairs

    # if we have a normal iterable use itertools
    import itertools
    a, b = itertools.tee(iterable)
    # pop the first element of the second item
    next(b)

    return zip(a, b)


try:
    # prefer the faster numpy version of multi_dot
    # only included in recent-ish version of numpy
    multi_dot = np.linalg.multi_dot
except AttributeError:
    log.debug('np.linalg.multi_dot not available, using fallback')

    def multi_dot(arrays):
        """
        Compute the dot product of two or more arrays in a single function call.
        In most versions of numpy this is included, this slower function is
        provided for backwards compatibility with ancient versions of numpy.
        """
        arrays = np.asanyarray(arrays)
        result = arrays[0].copy()
        for i in arrays[1:]:
            result = np.dot(result, i)
        return result


def diagonal_dot(a, b):
    """
    Dot product by row of a and b.

    There are a lot of ways to do this though
    performance varies very widely. This method
    uses a dot product to sum the row and avoids
    function calls if at all possible.

    Comparing performance of some equivalent versions:
    ```
    In [1]: import numpy as np; import trimesh

    In [2]: a = np.random.random((10000, 3))

    In [3]: b = np.random.random((10000, 3))

    In [4]: %timeit (a * b).sum(axis=1)
    1000 loops, best of 3: 181 us per loop

    In [5]: %timeit np.einsum('ij,ij->i', a, b)
    10000 loops, best of 3: 62.7 us per loop

    In [6]: %timeit np.diag(np.dot(a, b.T))
    1 loop, best of 3: 429 ms per loop

    In [7]: %timeit np.dot(a * b, np.ones(a.shape[1]))
    10000 loops, best of 3: 61.3 us per loop

    In [8]: %timeit trimesh.util.diagonal_dot(a, b)
    10000 loops, best of 3: 55.2 us per loop
    ```

    Parameters
    ------------
    a : (m, d) float
      First array
    b : (m, d) float
      Second array

    Returns
    -------------
    result : (m,) float
      Dot product of each row
    """
    # make sure `a` is numpy array
    # doing it for `a` will force the multiplication to
    # convert `b` if necessary and avoid function call otherwise
    a = np.asanyarray(a)
    # 3x faster than (a * b).sum(axis=1)
    # avoiding np.ones saves 5-10% sometimes
    return np.dot(a * b, [1.0] * a.shape[1])


def row_norm(data):
    """
    Compute the norm per-row of a numpy array.

    This is identical to np.linalg.norm(data, axis=1) but roughly
    three times faster due to being less general.

    In [3]: %timeit trimesh.util.row_norm(a)
    76.3 us +/- 651 ns per loop

    In [4]: %timeit np.linalg.norm(a, axis=1)
    220 us +/- 5.41 us per loop

    Parameters
    -------------
    data : (n, d) float
      Input 2D data to calculate per-row norm of

    Returns
    -------------
    norm : (n,) float
      Norm of each row of input array
    """
    return np.sqrt(np.dot(data ** 2, [1] * data.shape[1]))


def stack_3D(points, return_2D=False):
    """
    For a list of (n, 2) or (n, 3) points return them
    as (n, 3) 3D points, 2D points on the XY plane.

    Parameters
    ------------
    points :  (n, 2) or (n, 3) float
      Points in either 2D or 3D space
    return_2D : bool
      Were the original points 2D?

    Returns
    ----------
    points : (n, 3) float
      Points in space
    is_2D : bool
      [OPTIONAL] if source points were (n, 2)
    """
    points = np.asanyarray(points, dtype=np.float64)
    shape = points.shape

    if shape == (0,):
        is_2D = False
    elif len(shape) != 2:
        raise ValueError('Points must be 2D array!')
    elif shape[1] == 2:
        points = np.column_stack((
            points, np.zeros(len(points))))
        is_2D = True
    elif shape[1] == 3:
        is_2D = False
    else:
        raise ValueError('Points must be (n, 2) or (n, 3)!')

    if return_2D:
        return points, is_2D

    return points


def grid_arange(bounds, step):
    """
    Return a grid from an (2,dimension) bounds with samples step distance apart.

    Parameters
    ------------
    bounds: (2,dimension) list of [[min x, min y, etc], [max x, max y, etc]]
    step:   float, or (dimension) floats, separation between points

    Returns
    ---------
    grid: (n, dimension), points inside the specified bounds
    """
    bounds = np.asanyarray(bounds, dtype=np.float64)
    if len(bounds) != 2:
        raise ValueError('bounds must be (2, dimension!')

    # allow single float or per-dimension spacing
    step = np.asanyarray(step, dtype=np.float64)
    if step.shape == ():
        step = np.tile(step, bounds.shape[1])

    grid_elements = [np.arange(*b, step=s) for b, s in zip(bounds.T, step)]
    grid = np.vstack(np.meshgrid(*grid_elements, indexing='ij')
                     ).reshape(bounds.shape[1], -1).T
    return grid


def grid_linspace(bounds, count):
    """
    Return a grid spaced inside a bounding box with edges spaced using np.linspace.

    Parameters
    ------------
    bounds: (2,dimension) list of [[min x, min y, etc], [max x, max y, etc]]
    count:  int, or (dimension,) int, number of samples per side

    Returns
    ---------
    grid: (n, dimension) float, points in the specified bounds
    """
    bounds = np.asanyarray(bounds, dtype=np.float64)
    if len(bounds) != 2:
        raise ValueError('bounds must be (2, dimension!')

    count = np.asanyarray(count, dtype=np.int64)
    if count.shape == ():
        count = np.tile(count, bounds.shape[1])

    grid_elements = [np.linspace(*b, num=c) for b, c in zip(bounds.T, count)]
    grid = np.vstack(np.meshgrid(*grid_elements, indexing='ij')
                     ).reshape(bounds.shape[1], -1).T
    return grid


def multi_dict(pairs):
    """
    Given a set of key value pairs, create a dictionary.
    If a key occurs multiple times, stack the values into an array.

    Can be called like the regular dict(pairs) constructor

    Parameters
    ------------
    pairs: (n, 2) array of key, value pairs

    Returns
    ----------
    result: dict, with all values stored (rather than last with regular dict)

    """
    result = collections.defaultdict(list)
    for k, v in pairs:
        result[k].append(v)
    return result


def tolist(data):
    """
    Ensure that any arrays or dicts passed containing
    numpy arrays are properly converted to lists

    Parameters
    -------------
    data : any
      Usually a dict with some numpy arrays as values

    Returns
    ----------
    result : any
      JSON-serializable version of data
    """
    result = json.loads(jsonify(data))
    return result


def is_binary_file(file_obj):
    """
    Returns True if file has non-ASCII characters (> 0x7F, or 127)
    Should work in both Python 2 and 3
    """
    start = file_obj.tell()
    fbytes = file_obj.read(1024)
    file_obj.seek(start)
    is_str = isinstance(fbytes, str)
    for fbyte in fbytes:
        if is_str:
            code = ord(fbyte)
        else:
            code = fbyte
        if code > 127:
            return True
    return False


def distance_to_end(file_obj):
    """
    For an open file object how far is it to the end

    Parameters
    ------------
    file_obj: open file-like object

    Returns
    ----------
    distance: int, bytes to end of file
    """
    position_current = file_obj.tell()
    file_obj.seek(0, 2)
    position_end = file_obj.tell()
    file_obj.seek(position_current)
    distance = position_end - position_current
    return distance


def decimal_to_digits(decimal, min_digits=None):
    """
    Return the number of digits to the first nonzero decimal.

    Parameters
    -----------
    decimal:    float
    min_digits: int, minimum number of digits to return

    Returns
    -----------

    digits: int, number of digits to the first nonzero decimal
    """
    digits = abs(int(np.log10(decimal)))
    if min_digits is not None:
        digits = np.clip(digits, min_digits, 20)
    return digits


def attach_to_log(level=logging.DEBUG,
                  handler=None,
                  loggers=None,
                  colors=True,
                  capture_warnings=True,
                  blacklist=None):
    """
    Attach a stream handler to all loggers.

    Parameters
    ------------
    level : enum
      Logging level, like logging.INFO
    handler : None or logging.Handler
      Handler to attach
    loggers : None or (n,) logging.Logger
      If None, will try to attach to all available
    colors : bool
      If True try to use colorlog formatter
    blacklist : (n,) str
      Names of loggers NOT to attach to
    """

    # default blacklist includes ipython debugging stuff
    if blacklist is None:
        blacklist = ['TerminalIPythonApp',
                     'PYREADLINE',
                     'pyembree',
                     'shapely',
                     'matplotlib',
                     'parso']

    # make sure we log warnings from the warnings module
    logging.captureWarnings(capture_warnings)

    # create a basic formatter
    formatter = logging.Formatter(
        "[%(asctime)s] %(levelname)-7s (%(filename)s:%(lineno)3s) %(message)s",
        "%Y-%m-%d %H:%M:%S")
    if colors:
        try:
            from colorlog import ColoredFormatter
            formatter = ColoredFormatter(
                ("%(log_color)s%(levelname)-8s%(reset)s " +
                 "%(filename)17s:%(lineno)-4s  %(blue)4s%(message)s"),
                datefmt=None,
                reset=True,
                log_colors={'DEBUG': 'cyan',
                            'INFO': 'green',
                            'WARNING': 'yellow',
                            'ERROR': 'red',
                            'CRITICAL': 'red'})
        except ImportError:
            pass

    # if no handler was passed use a StreamHandler
    if handler is None:
        handler = logging.StreamHandler()

    # add the formatters and set the level
    handler.setFormatter(formatter)
    handler.setLevel(level)

    # if nothing passed use all available loggers
    if loggers is None:
        # de-duplicate loggers using a set
        loggers = set(logging.Logger.manager.loggerDict.values())
    # add the warnings logging
    loggers.add(logging.getLogger('py.warnings'))

    # disable pyembree warnings
    logging.getLogger('pyembree').disabled = True

    # loop through all available loggers
    for logger in loggers:
        # skip loggers on the blacklist
        if (logger.__class__.__name__ != 'Logger' or
                any(logger.name.startswith(b) for b in blacklist)):
            continue
        logger.addHandler(handler)
        logger.setLevel(level)

    # set nicer numpy print options
    np.set_printoptions(precision=5, suppress=True)


def stack_lines(indices):
    """
    Stack a list of values that represent a polyline into
    individual line segments with duplicated consecutive values.

    Parameters
    ------------
    indices : (m,) any
      List of items to be stacked

    Returns
    ---------
    stacked : (n, 2) any
      Stacked items

    Examples
    ----------
    In [1]: trimesh.util.stack_lines([0, 1, 2])
    Out[1]:
    array([[0, 1],
           [1, 2]])

    In [2]: trimesh.util.stack_lines([0, 1, 2, 4, 5])
    Out[2]:
    array([[0, 1],
           [1, 2],
           [2, 4],
           [4, 5]])

    In [3]: trimesh.util.stack_lines([[0, 0], [1, 1], [2, 2], [3, 3]])
    Out[3]:
    array([[0, 0],
           [1, 1],
           [1, 1],
           [2, 2],
           [2, 2],
           [3, 3]])

    """
    indices = np.asanyarray(indices)
    if len(indices) == 0:
        return np.array([])
    elif is_sequence(indices[0]):
        shape = (-1, len(indices[0]))
    else:
        shape = (-1, 2)
    return np.column_stack((indices[:-1],
                            indices[1:])).reshape(shape)


def append_faces(vertices_seq, faces_seq):
    """
    Given a sequence of zero-indexed faces and vertices
    combine them into a single array of faces and
    a single array of vertices.

    Parameters
    -----------
    vertices_seq : (n, ) sequence of (m, d) float
      Multiple arrays of verticesvertex arrays
    faces_seq : (n, ) sequence of (p, j) int
      Zero indexed faces for matching vertices

    Returns
    ----------
    vertices : (i, d) float
      Points in space
    faces : (j, 3) int
      Reference vertex indices
    """
    # the length of each vertex array
    vertices_len = np.array([len(i) for i in vertices_seq])
    # how much each group of faces needs to be offset
    face_offset = np.append(0, np.cumsum(vertices_len)[:-1])

    new_faces = []
    for offset, faces in zip(face_offset, faces_seq):
        if len(faces) == 0:
            continue
        # apply the index offset
        new_faces.append(faces + offset)
    # stack to clean (n, 3) float
    vertices = vstack_empty(vertices_seq)
    # stack to clean (n, 3) int
    faces = vstack_empty(new_faces)

    return vertices, faces


def array_to_string(array,
                    col_delim=' ',
                    row_delim='\n',
                    digits=8,
                    value_format='{}'):
    """
    Convert a 1 or 2D array into a string with a specified number
    of digits and delimiter. The reason this exists is that the
    basic numpy array to string conversions are surprisingly bad.

    Parameters
    ------------
    array : (n,) or (n, d) float or int
       Data to be converted
       If shape is (n,) only column delimiter will be used
    col_delim : str
      What string should separate values in a column
    row_delim : str
      What string should separate values in a row
    digits : int
      How many digits should floating point numbers include
    value_format : str
       Format string for each value or sequence of values
       If multiple values per value_format it must divide
       into array evenly.

    Returns
    ----------
    formatted : str
       String representation of original array
    """
    # convert inputs to correct types
    array = np.asanyarray(array)
    digits = int(digits)
    row_delim = str(row_delim)
    col_delim = str(col_delim)
    value_format = str(value_format)

    # abort for non-flat arrays
    if len(array.shape) > 2:
        raise ValueError('conversion only works on 1D/2D arrays not %s!',
                         str(array.shape))

    # abort for structured arrays
    if array.dtype.names is not None:
        raise ValueError(
            'array is  structured, use structured_array_to_string instead')

    # allow a value to be repeated in a value format
    repeats = value_format.count('{')

    if array.dtype.kind in ['i', 'u']:
        # integer types don't need a specified precision
        format_str = value_format + col_delim
    elif array.dtype.kind == 'f':
        # add the digits formatting to floats
        format_str = value_format.replace(
            '{}', '{:.' + str(digits) + 'f}') + col_delim
    else:
        raise ValueError('dtype %s not convertible!',
                         array.dtype.name)

    # length of extra delimiters at the end
    end_junk = len(col_delim)
    # if we have a 2D array add a row delimiter
    if len(array.shape) == 2:
        format_str *= array.shape[1]
        # cut off the last column delimiter and add a row delimiter
        format_str = format_str[:-len(col_delim)] + row_delim
        end_junk = len(row_delim)

    # expand format string to whole array
    format_str *= len(array)

    # if an array is repeated in the value format
    # do the shaping here so we don't need to specify indexes
    shaped = np.tile(array.reshape((-1, 1)),
                     (1, repeats)).reshape(-1)

    # run the format operation and remove the extra delimiters
    formatted = format_str.format(*shaped)[:-end_junk]

    return formatted


def structured_array_to_string(array,
                               col_delim=' ',
                               row_delim='\n',
                               digits=8,
                               value_format='{}'):
    """
    Convert an unstructured array into a string with a specified
    number of digits and delimiter. The reason thisexists is
    that the basic numpy array to string conversionsare
    surprisingly bad.

    Parameters
    ------------
    array : (n,) or (n, d) float or int
       Data to be converted
       If shape is (n,) only column delimiter will be used
    col_delim : str
      What string should separate values in a column
    row_delim : str
      What string should separate values in a row
    digits : int
      How many digits should floating point numbers include
    value_format : str
       Format string for each value or sequence of values
       If multiple values per value_format it must divide
       into array evenly.

    Returns
    ----------
    formatted : str
       String representation of original array
    """
    # convert inputs to correct types
    array = np.asanyarray(array)
    digits = int(digits)
    row_delim = str(row_delim)
    col_delim = str(col_delim)
    value_format = str(value_format)

    # abort for non-flat arrays
    if len(array.shape) > 1:
        raise ValueError('conversion only works on 1D/2D arrays not %s!',
                         str(array.shape))

    # abort for unstructured arrays
    if array.dtype.names is None:
        raise ValueError(
            'array is not structured, use array_to_string instead')

    # do not allow a value to be repeated in a value format
    if value_format.count('{') > 1:
        raise ValueError(
            'value_format %s is invalid, repeating unstructured array '
            + 'values is unsupported', value_format)

    format_str = ''
    for name in array.dtype.names:
        kind = array[name].dtype.kind
        element_row_length = (
            array[name].shape[1] if len(array[name].shape) == 2 else 1)
        if kind in ['i', 'u']:
            # integer types need a no-decimal formatting
            element_format_str = value_format.replace(
                '{}', '{:0.0f}') + col_delim
        elif kind == 'f':
            # add the digits formatting to floats
            element_format_str = value_format.replace(
                '{}', '{:.' + str(digits) + 'f}') + col_delim
        else:
            raise ValueError('dtype %s not convertible!',
                             array.dtype)
        format_str += element_row_length * element_format_str

    # length of extra delimiters at the end
    format_str = format_str[:-len(col_delim)] + row_delim
    # expand format string to whole array
    format_str *= len(array)

    # loop through flat fields and flatten to single array
    count = len(array)
    # will upgrade everything to a float
    flattened = np.hstack(
        [array[k].reshape((count, -1))
         for k in array.dtype.names]).reshape(-1)

    # run the format operation and remove the extra delimiters
    formatted = format_str.format(*flattened)[:-len(row_delim)]

    return formatted


def array_to_encoded(array, dtype=None, encoding='base64'):
    """
    Export a numpy array to a compact serializable dictionary.

    Parameters
    ------------
    array : array
      Any numpy array
    dtype : str or None
      Optional dtype to encode array
    encoding : str
      'base64' or 'binary'

    Returns
    ---------
    encoded : dict
      Has keys:
      'dtype':  str, of dtype
      'shape':  tuple of shape
      'base64': str, base64 encoded string
    """
    array = np.asanyarray(array)
    shape = array.shape
    # ravel also forces contiguous
    flat = np.ravel(array)
    if dtype is None:
        dtype = array.dtype

    encoded = {'dtype': np.dtype(dtype).str,
               'shape': shape}
    if encoding in ['base64', 'dict64']:
        packed = base64.b64encode(flat.astype(dtype).tobytes())
        if hasattr(packed, 'decode'):
            packed = packed.decode('utf-8')
        encoded['base64'] = packed
    elif encoding == 'binary':
        encoded['binary'] = array.tobytes(order='C')
    else:
        raise ValueError('encoding {} is not available!'.format(encoding))
    return encoded


def decode_keys(store, encoding='utf-8'):
    """
    If a dictionary has keys that are bytes decode them to a str.

    Parameters
    ------------
    store : dict
      Dictionary with data

    Returns
    ---------
    result : dict
      Values are untouched but keys that were bytes
      are converted to ASCII strings.

    Example
    -----------
    In [1]: d
    Out[1]: {1020: 'nah', b'hi': 'stuff'}

    In [2]: trimesh.util.decode_keys(d)
    Out[2]: {1020: 'nah', 'hi': 'stuff'}
    """
    keys = store.keys()
    for key in keys:
        if hasattr(key, 'decode'):
            decoded = key.decode(encoding)
            if key != decoded:
                store[key.decode(encoding)] = store[key]
                store.pop(key)
    return store


def comment_strip(text, starts_with='#', new_line='\n'):
    """
    Strip comments from a text block.

    Parameters
    -----------
    text : str
      Text to remove comments from
    starts_with : str
      Character or substring that starts a comment
    new_line : str
      Character or substring that ends a comment

    Returns
    -----------
    stripped : str
      Text with comments stripped
    """
    # if not contained exit immediately
    if starts_with not in text:
        return text

    # start by splitting into chunks by the comment indicator
    split = (text + new_line).split(starts_with)

    # special case files that start with a comment
    if text.startswith(starts_with):
        lead = ''
    else:
        lead = split[0]

    # take each comment up until the newline
    removed = [i.split(new_line, 1) for i in split]
    # add the leading string back on
    result = lead + new_line + new_line.join(
        i[1] for i in removed
        if len(i) > 1 and len(i[1]) > 0)
    # strip leading and trailing whitespace
    result = result.strip()

    return result


def encoded_to_array(encoded):
    """
    Turn a dictionary with base64 encoded strings back into a numpy array.

    Parameters
    ------------
    encoded : dict
      Has keys:
        dtype: string of dtype
        shape: int tuple of shape
        base64: base64 encoded string of flat array
        binary:  decode result coming from numpy.tobytes

    Returns
    ----------
    array: numpy array
    """

    if not isinstance(encoded, dict):
        if is_sequence(encoded):
            as_array = np.asanyarray(encoded)
            return as_array
        else:
            raise ValueError('Unable to extract numpy array from input')

    encoded = decode_keys(encoded)

    dtype = np.dtype(encoded['dtype'])
    if 'base64' in encoded:
        array = np.frombuffer(base64.b64decode(encoded['base64']),
                              dtype)
    elif 'binary' in encoded:
        array = np.frombuffer(encoded['binary'],
                              dtype=dtype)
    if 'shape' in encoded:
        array = array.reshape(encoded['shape'])
    return array


def is_instance_named(obj, name):
    """
    Given an object, if it is a member of the class 'name',
    or a subclass of 'name', return True.

    Parameters
    ------------
    obj : instance
      Some object of some class
    name: str
      The name of the class we want to check for

    Returns
    ---------
    is_instance : bool
      Whether the object is a member of the named class
    """
    try:
        if isinstance(name, list):
            return any(is_instance_named(obj, i) for i in name)
        else:
            type_named(obj, name)
        return True
    except ValueError:
        return False


def type_bases(obj, depth=4):
    """
    Return the bases of the object passed.
    """
    bases = collections.deque([list(obj.__class__.__bases__)])
    for i in range(depth):
        bases.append([i.__base__ for i in bases[-1] if i is not None])
    try:
        bases = np.hstack(bases)
    except IndexError:
        bases = []
    return [i for i in bases if hasattr(i, '__name__')]


def type_named(obj, name):
    """
    Similar to the type() builtin, but looks in class bases
    for named instance.

    Parameters
    ------------
    obj : any
      Object to look for class of
    name : str
      Nnme of class

    Returns
    ----------
    class : Optional[Callable]
      Camed class, or None
    """
    # if obj is a member of the named class, return True
    name = str(name)
    if obj.__class__.__name__ == name:
        return obj.__class__
    for base in type_bases(obj):
        if base.__name__ == name:
            return base
    raise ValueError('Unable to extract class of name ' + name)


def concatenate(a, b=None):
    """
    Concatenate two or more meshes.

    Parameters
    ------------
    a : trimesh.Trimesh
      Mesh or list of meshes to be concatenated
      object, or list of such
    b : trimesh.Trimesh
      Mesh or list of meshes to be concatenated

    Returns
    ----------
    result : trimesh.Trimesh
      Concatenated mesh
    """

    # get a flat list of meshes
    flat = []
    if a is not None:
        if is_sequence(a):
            flat.extend(a)
        else:
            flat.append(a)
    if b is not None:
        if is_sequence(b):
            flat.extend(b)
        else:
            flat.append(b)

    if len(flat) == 1:
        # if there is only one mesh just return the first
        return flat[0].copy()
    elif len(flat) == 0:
        # if there are no meshes return an empty list
        return []

    is_mesh = [f for f in flat if is_instance_named(f, 'Trimesh')]
    is_path = [f for f in flat if is_instance_named(f, 'Path')]

    if len(is_path) > len(is_mesh):
        from .path.util import concatenate as concatenate_path
        return concatenate_path(is_path)

    if len(is_mesh) == 0:
        return []

    # extract the trimesh type to avoid a circular import
    # and assert that all inputs are Trimesh objects
    trimesh_type = type_named(is_mesh[0], 'Trimesh')

    # append faces and vertices of meshes
    vertices, faces = append_faces(
        [m.vertices.copy() for m in is_mesh],
        [m.faces.copy() for m in is_mesh])

    # only save face normals if already calculated
    face_normals = None
    if all('face_normals' in m._cache for m in is_mesh):
        face_normals = np.vstack(
            [m.face_normals for m in is_mesh])

    try:
        # concatenate visuals
        visual = is_mesh[0].visual.concatenate(
            [m.visual for m in is_mesh[1:]])
    except BaseException:
        log.debug('failed to combine visuals', exc_info=True)
        visual = None

    # create the mesh object
    return trimesh_type(vertices=vertices,
                        faces=faces,
                        face_normals=face_normals,
                        visual=visual,
                        process=False)


def submesh(mesh,
            faces_sequence,
            repair=True,
            only_watertight=False,
            min_faces=None,
            append=False):
    """
    Return a subset of a mesh.

    Parameters
    ------------
    mesh : Trimesh
        Source mesh to take geometry from
    faces_sequence : sequence (p,) int
        Indexes of mesh.faces
    repair : bool
        Try to make submeshes watertight
    only_watertight : bool
        Only return submeshes which are watertight
    append : bool
        Return a single mesh which has the faces appended,
        if this flag is set, only_watertight is ignored

    Returns
    ---------
    if append : Trimesh object
    else        list of Trimesh objects
    """
    # evaluate generators so we can escape early
    faces_sequence = list(faces_sequence)

    if len(faces_sequence) == 0:
        return []

    # avoid nuking the cache on the original mesh
    original_faces = mesh.faces.view(np.ndarray)
    original_vertices = mesh.vertices.view(np.ndarray)

    faces = []
    vertices = []
    normals = []
    visuals = []

    # for reindexing faces
    mask = np.arange(len(original_vertices))

    for index in faces_sequence:
        # sanitize indices in case they are coming in as a set or tuple
        index = np.asanyarray(index)
        if len(index) == 0:
            # regardless of type empty arrays are useless
            continue
        if index.dtype.kind == 'b':
            # if passed a bool with no true continue
            if not index.any():
                continue
            # if fewer faces than minimum
            if min_faces is not None and index.sum() < min_faces:
                continue
        elif min_faces is not None and len(index) < min_faces:
            continue

        current = original_faces[index]
        unique = np.unique(current.reshape(-1))

        # redefine face indices from zero
        mask[unique] = np.arange(len(unique))
        normals.append(mesh.face_normals[index])
        faces.append(mask[current])
        vertices.append(original_vertices[unique])

        try:
            visuals.append(mesh.visual.face_subset(index))
        except BaseException:
            visuals = None

    if len(vertices) == 0:
        return np.array([])

    # we use type(mesh) rather than importing Trimesh from base
    # to avoid a circular import
    trimesh_type = type_named(mesh, 'Trimesh')
    if append:
        visual = None
        try:
            visuals = np.array(visuals)
            visual = visuals[0].concatenate(visuals[1:])
        except BaseException:
            pass
        # re-index faces and stack
        vertices, faces = append_faces(vertices, faces)
        appended = trimesh_type(
            vertices=vertices,
            faces=faces,
            face_normals=np.vstack(normals),
            visual=visual,
            process=False)
        return appended

    if visuals is None:
        visuals = [None] * len(vertices)

    # generate a list of Trimesh objects
    result = [trimesh_type(
        vertices=v,
        faces=f,
        face_normals=n,
        visual=c,
        metadata=copy.deepcopy(mesh.metadata),
        process=False) for v, f, n, c in zip(vertices,
                                             faces,
                                             normals,
                                             visuals)]
    result = np.array(result)
    if only_watertight or repair:
        # fill_holes will attempt a repair and returns the
        # watertight status at the end of the repair attempt
        watertight = np.array([i.fill_holes() and len(i.faces) >= 4
                               for i in result])
    if only_watertight:
        # remove unrepairable meshes
        result = result[watertight]

    return result


def zero_pad(data, count, right=True):
    """
    Parameters
    ------------
    data : (n,)
      1D array
    count : int
      Minimum length of result array

    Returns
    ---------
    padded : (m,)
      1D array where m >= count
    """
    if len(data) == 0:
        return np.zeros(count)
    elif len(data) < count:
        padded = np.zeros(count)
        if right:
            padded[-len(data):] = data
        else:
            padded[:len(data)] = data
        return padded
    else:
        return np.asanyarray(data)


def jsonify(obj, **kwargs):
    """
    A version of json.dumps that can handle numpy arrays
    by creating a custom encoder for numpy dtypes.

    Parameters
    --------------
    obj : list, dict
      A JSON-serializable blob
    kwargs : dict
      Passed to json.dumps

    Returns
    --------------
    dumped : str
      JSON dump of obj
    """
    class EdgeEncoder(json.JSONEncoder):
        def default(self, obj):
            # will work for numpy.ndarrays
            # as well as their int64/etc objects
            if hasattr(obj, 'tolist'):
                return obj.tolist()
            elif hasattr(obj, 'timestamp'):
                return obj.timestamp()
            return json.JSONEncoder.default(self, obj)
    # run the dumps using our encoder
    return json.dumps(obj, cls=EdgeEncoder, **kwargs)


def convert_like(item, like):
    """
    Convert an item to have the dtype of another item

    Parameters
    ------------
    item : any
      Item to be converted
    like : any
      Object with target dtype
      If None, item is returned unmodified

    Returns
    ----------
    result: item, but in dtype of like
    """
    # if it's a numpy array
    if isinstance(like, np.ndarray):
        return np.asanyarray(item, dtype=like.dtype)

    # if it's already the desired type just return it
    if isinstance(item, like.__class__) or is_none(like):
        return item

    # if it's an array with one item return it
    if (is_sequence(item) and len(item) == 1 and
            isinstance(item[0], like.__class__)):
        return item[0]

    if (isinstance(item, str) and
        like.__class__.__name__ == 'Polygon' and
            item.startswith('POLYGON')):
        # break our rule on imports but only a little bit
        # the import was a WKT serialized polygon
        from shapely import wkt
        return wkt.loads(item)

    # otherwise just run the conversion
    item = like.__class__(item)

    return item


def bounds_tree(bounds):
    """
    Given a set of axis aligned bounds create an r-tree for
    broad-phase collision detection.

    Parameters
    ------------
    bounds : (n, 2D) or (n, 2, D) float
      Non-interleaved bounds where D=dimension
      E.G a 2D bounds tree:
      [(minx, miny, maxx, maxy), ...]

    Returns
    ---------
    tree : Rtree
      Tree containing bounds by index
    """
    # rtree is a soft dependency
    import rtree

    # make sure we've copied bounds
    bounds = np.array(bounds, dtype=np.float64, copy=True)
    if len(bounds.shape) == 3:
        # should be min-max per bound
        if bounds.shape[1] != 2:
            raise ValueError('bounds not (n, 2, dimension)!')
        # reshape to one-row-per-hyperrectangle
        bounds = bounds.reshape((len(bounds), -1))
    elif len(bounds.shape) != 2 or bounds.size == 0:
        raise ValueError('Bounds must be (n, dimension * 2)!')

    # check to make sure we have correct shape
    dimension = bounds.shape[1]
    if (dimension % 2) != 0:
        raise ValueError('Bounds must be (n,dimension*2)!')
    dimension = int(dimension / 2)

    # some versions of rtree screw up indexes on stream loading
    # do a test here so we know if we are free to use stream loading
    # or if we have to do a loop to insert things which is 5x slower
    rtree_test = rtree.index.Index(
        [(1564, [0, 0, 0, 10, 10, 10], None)],
        properties=rtree.index.Property(dimension=3))
    rtree_stream_ok = next(rtree_test.intersection(
        [1, 1, 1, 2, 2, 2])) == 1564

    properties = rtree.index.Property(dimension=dimension)
    if rtree_stream_ok:
        # stream load was verified working on import above
        tree = rtree.index.Index(zip(np.arange(len(bounds)),
                                     bounds,
                                     [None] * len(bounds)),
                                 properties=properties)
    else:
        # in some rtree versions stream loading goofs the index
        log.warning('rtree stream loading broken! Try upgrading rtree!')
        tree = rtree.index.Index(properties=properties)
        for i, b in enumerate(bounds):
            tree.insert(i, b)
    return tree


def wrap_as_stream(item):
    """
    Wrap a string or bytes object as a file object.

    Parameters
    ------------
    item: str or bytes
      Item to be wrapped

    Returns
    ---------
    wrapped : file-like object
      Contains data from item
    """
    if not PY3:
        # in python 2 StringIO handles bytes and str
        return StringIO(item)
    if isinstance(item, str):
        return StringIO(item)
    elif isinstance(item, bytes):
        return BytesIO(item)
    raise ValueError('{} is not wrappable!'.format(type(item).__name__))


def sigfig_round(values, sigfig=1):
    """
    Round a single value to a specified number of significant figures.

    Parameters
    ------------
    values : float
      Value to be rounded
    sigfig : int
      Number of significant figures to reduce to

    Returns
    ----------
    rounded : float
      Value rounded to the specified number of significant figures


    Examples
    ----------
    In [1]: trimesh.util.round_sigfig(-232453.00014045456, 1)
    Out[1]: -200000.0

    In [2]: trimesh.util.round_sigfig(.00014045456, 1)
    Out[2]: 0.0001

    In [3]: trimesh.util.round_sigfig(.00014045456, 4)
    Out[3]: 0.0001405
    """
    as_int, multiplier = sigfig_int(values, sigfig)
    rounded = as_int * (10 ** multiplier)

    return rounded


def sigfig_int(values, sigfig):
    """
    Convert a set of floating point values into integers
    with a specified number of significant figures and an
    exponent.

    Parameters
    ------------
    values : (n,) float or int
      Array of values
    sigfig : (n,) int
      Number of significant figures to keep

    Returns
    ------------
    as_int : (n,) int
      Every value[i] has sigfig[i] digits
    multiplier : (n, int)
      Exponent, so as_int * 10 ** multiplier is
      the same order of magnitude as the input
    """
    values = np.asanyarray(values).reshape(-1)
    sigfig = np.asanyarray(sigfig, dtype=np.int64).reshape(-1)

    if sigfig.shape != values.shape:
        raise ValueError('sigfig must match identifier')

    exponent = np.zeros(len(values))
    nonzero = np.abs(values) > TOL_ZERO
    exponent[nonzero] = np.floor(np.log10(np.abs(values[nonzero])))

    multiplier = exponent - sigfig + 1
    as_int = (values / (10**multiplier)).round().astype(np.int64)

    return as_int, multiplier


def decompress(file_obj, file_type):
    """
    Given an open file object and a file type, return all components
    of the archive as open file objects in a dict.

    Parameters
    ------------
    file_obj : file-like
      Containing compressed data
    file_type : str
      File extension, 'zip', 'tar.gz', etc

    Returns
    ---------
    decompressed : dict
      Data from archive in format {file name : file-like}
    """

    file_type = str(file_type).lower()
    if isinstance(file_obj, bytes):
        file_obj = wrap_as_stream(file_obj)

    if file_type.endswith('zip'):
        archive = zipfile.ZipFile(file_obj)
        return {name: wrap_as_stream(archive.read(name))
                for name in archive.namelist()}
    if 'tar' in file_type[-6:]:
        import tarfile
        archive = tarfile.open(fileobj=file_obj, mode='r')
        return {name: archive.extractfile(name)
                for name in archive.getnames()}
    raise ValueError('Unsupported type passed!')


def compress(info, **kwargs):
    """
    Compress data stored in a dict.

    Parameters
    -----------
    info : dict
      Data to compress in form:
      {file name in archive: bytes or file-like object}
    kwargs : dict
      Passed to zipfile.ZipFile
    Returns
    -----------
    compressed : bytes
      Compressed file data
    """
    if PY3:
        file_obj = BytesIO()
    else:
        file_obj = StringIO()

    with zipfile.ZipFile(
            file_obj,
            mode='w',
            compression=zipfile.ZIP_DEFLATED, **kwargs) as zipper:
        for name, data in info.items():
            if hasattr(data, 'read'):
                # if we were passed a file object, read it
                data = data.read()
            zipper.writestr(name, data)
    file_obj.seek(0)
    compressed = file_obj.read()
    return compressed


def split_extension(file_name, special=None):
    """
    Find the file extension of a file name, including support for
    special case multipart file extensions (like .tar.gz)

    Parameters
    ------------
    file_name : str
      File name
    special : list of str
      Multipart extensions
      eg: ['tar.bz2', 'tar.gz']

    Returns
    ----------
    extension : str
      Last characters after a period, or
      a value from 'special'
    """
    file_name = str(file_name)

    if special is None:
        special = ['tar.bz2', 'tar.gz']
    if file_name.endswith(tuple(special)):
        for end in special:
            if file_name.endswith(end):
                return end
    return file_name.split('.')[-1]


def triangle_strips_to_faces(strips):
    """
    Convert a sequence of triangle strips to (n, 3) faces.

    Processes all strips at once using np.concatenate and is significantly
    faster than loop-based methods.

    From the OpenGL programming guide describing a single triangle
    strip [v0, v1, v2, v3, v4]:

    Draws a series of triangles (three-sided polygons) using vertices
    v0, v1, v2, then v2, v1, v3  (note the order), then v2, v3, v4,
    and so on. The ordering is to ensure that the triangles are all
    drawn with the same orientation so that the strip can correctly form
    part of a surface.

    Parameters
    ------------
    strips: (n,) list of (m,) int
      Vertex indices

    Returns
    ------------
    faces : (m, 3) int
      Vertex indices representing triangles
    """

    # save the length of each list in the list of lists
    lengths = np.array([len(i) for i in strips])
    # looping through a list of lists is extremely slow
    # combine all the sequences into a blob we can manipulate
    blob = np.concatenate(strips)

    # preallocate and slice the blob into rough triangles
    tri = np.zeros((len(blob) - 2, 3), dtype=np.int64)
    for i in range(3):
        tri[:len(blob) - 3, i] = blob[i:-3 + i]
    # the last triangle is left off from the slicing, add it back
    tri[-1] = blob[-3:]

    # remove the triangles which were implicit but not actually there
    # because we combined everything into one big array for speed
    length_index = np.cumsum(lengths)[:-1]
    keep = np.ones(len(tri), dtype=bool)
    keep[length_index - 2] = False
    keep[length_index - 1] = False
    tri = tri[keep]

    # flip every other triangle so they generate correct normals/winding
    length_index = np.append(0, np.cumsum(lengths - 2))
    flip = np.zeros(length_index[-1], dtype=bool)
    for i in range(len(length_index) - 1):
        flip[length_index[i] + 1:length_index[i + 1]][::2] = True
    tri[flip] = np.fliplr(tri[flip])

    return tri


def triangle_fans_to_faces(fans):
    """
    Convert fans of m + 2 vertex indices in fan format to m triangles

    Parameters
    ----------
    fans: (n,) list of (m + 2,) int
      Vertex indices

    Returns
    -------
    faces: (m, 3) int
      Vertex indices representing triangles
    """

    faces = [np.transpose([
                fan[0]*np.ones(len(fan) - 2, dtype=int),
                fan[1:-1],
                fan[2:]
            ]) for fan in fans]
    return np.concatenate(faces, axis=1)


def vstack_empty(tup):
    """
    A thin wrapper for numpy.vstack that ignores empty lists.

    Parameters
    ------------
    tup : tuple or list of arrays
      With the same number of columns

    Returns
    ------------
    stacked : (n, d) array
      With same number of columns as
      constituent arrays.
    """
    # filter out empty arrays
    stackable = [i for i in tup if len(i) > 0]
    # if we only have one array just return it
    if len(stackable) == 1:
        return np.asanyarray(stackable[0])
    # if we have nothing return an empty numpy array
    elif len(stackable) == 0:
        return np.array([])
    # otherwise just use vstack as normal
    return np.vstack(stackable)


def write_encoded(file_obj,
                  stuff,
                  encoding='utf-8'):
    """
    If a file is open in binary mode and a
    string is passed, encode and write.

    If a file is open in text mode and bytes are
    passed decode bytes to str and write.

    Assumes binary mode if file_obj does not have
    a 'mode' attribute (e.g. io.BufferedRandom).

    Parameters
    -----------
    file_obj : file object
      With 'write' and 'mode'
    stuff :  str or bytes
      Stuff to be written
    encoding : str
      Encoding of text
    """
    binary_file = 'b' in getattr(file_obj, 'mode', 'b')
    string_stuff = isinstance(stuff, basestring)
    binary_stuff = isinstance(stuff, bytes)

    if not PY3:
        file_obj.write(stuff)
    elif binary_file and string_stuff:
        file_obj.write(stuff.encode(encoding))
    elif not binary_file and binary_stuff:
        file_obj.write(stuff.decode(encoding))
    else:
        file_obj.write(stuff)
    file_obj.flush()
    return stuff


def unique_id(length=12):
    """
    Generate a random alphanumeric unique identifier
    using UUID logic.

    Parameters
    ------------
    length : int
      Length of desired identifier

    Returns
    ------------
    unique : str
      Unique alphanumeric identifier
    """
    return uuid.UUID(int=random.getrandbits(128),
                     version=4).hex[:length]


def generate_basis(z, epsilon=1e-12):
    """
    Generate an arbitrary basis (also known as a coordinate frame)
    from a given z-axis vector.

    Parameters
    ------------
    z : (3,) float
      A vector along the positive z-axis.
    epsilon : float
      Numbers smaller than this considered zero.

    Returns
    ---------
    x : (3,) float
      Vector along x axis.
    y : (3,) float
      Vector along y axis.
    z : (3,) float
      Vector along z axis.
    """
    # get a copy of input vector
    z = np.array(z, dtype=np.float64, copy=True)
    # must be a 3D vector
    if z.shape != (3,):
        raise ValueError('z must be (3,) float!')

    z_norm = np.linalg.norm(z)
    if z_norm < epsilon:
        return np.eye(3)

    # normalize vector in-place
    z /= z_norm
    # X as arbitrary perpendicular vector
    x = np.array([-z[1], z[0], 0.0])
    # avoid degenerate case
    x_norm = np.linalg.norm(x)
    if x_norm < epsilon:
        # this means that
        # so a perpendicular X is just X
        x = np.array([-z[2], z[1], 0.0])
        x /= np.linalg.norm(x)
    else:
        # otherwise normalize X in-place
        x /= x_norm
    # get perpendicular Y with cross product
    y = np.cross(z, x)
    # append result values into (3, 3) vector
    result = np.array([x, y, z], dtype=np.float64)

    if _STRICT:
        # run checks to make sure axis are perpendicular
        assert np.abs(np.dot(x, z)) < 1e-8
        assert np.abs(np.dot(y, z)) < 1e-8
        assert np.abs(np.dot(x, y)) < 1e-8
        # all vectors should be unit vector
        assert np.allclose(np.linalg.norm(result, axis=1), 1.0)

    return result


def isclose(a, b, atol):
    """
    A replacement for np.isclose that does fewer checks
    and validation and as a result is roughly 4x faster.

    Note that this is used in tight loops, and as such
    a and b MUST be np.ndarray, not list or "array-like"

    Parameters
    ------------
    a : np.ndarray
      To be compared
    b : np.ndarray
      To be compared
    atol : float
      Acceptable distance between `a` and `b` to be "close"

    Returns
    -----------
    close : np.ndarray, bool
      Per-element closeness
    """
    diff = a - b
    close = np.logical_and(diff > -atol, diff < atol)

    return close


def allclose(a, b, atol=1e-8):
    """
    A replacement for np.allclose that does few checks
    and validation and as a result is faster.

    Parameters
    ------------
    a : np.ndarray
      To be compared
    b : np.ndarray
      To be compared
    atol : float
      Acceptable distance between `a` and `b` to be "close"

    Returns
    -----------
    bool indicating if all elements are within `atol`.
    """
    return float((a - b).ptp()) < atol


class FunctionRegistry(Mapping):
    """
    Non-overwritable mapping of string keys to functions.

    This allows external packages to register additional implementations
    of common functionality without risk of breaking implementations provided
    by trimesh.

    See trimesh.voxel.morphology for example usage.
    """

    def __init__(self, **kwargs):
        self._dict = {}
        for k, v in kwargs.items():
            self[k] = v

    def __getitem__(self, key):
        return self._dict[key]

    def __setitem__(self, key, value):
        if not isinstance(key, str):
            raise ValueError('key must be a string, got %s' % str(key))
        if key in self:
            raise KeyError('Cannot set new value to existing key %s' % key)
        if not callable(value):
            raise ValueError('Cannot set value which is not callable.')
        self._dict[key] = value

    def __iter__(self):
        return iter(self._dict)

    def __len__(self):
        return len(self._dict)

    def __contains__(self, key):
        return key in self._dict

    def __call__(self, key, *args, **kwargs):
        return self[key](*args, **kwargs)


class TemporaryDirectory(object):
    """
    Same basic usage as tempfile.TemporaryDirectory
    but functional in Python 2.7+.

    Example
    ---------
    ```
    with trimesh.util.TemporaryDirectory() as path:
       writable = os.path.join(path, 'hi.txt')
    ```
    """

    def __enter__(self):
        self.path = tempfile.mkdtemp()
        return self.path

    def __exit__(self, *args, **kwargs):
        shutil.rmtree(self.path)


def decode_text(text, initial='utf-8'):
    """
    Try to decode byte input as a string.

    Tries initial guess (UTF-8) then if that fails it
    uses chardet to try another guess before failing.

    Parameters
    ------------
    text : bytes
      Data that might be a string
    initial : str
      Initial guess for text encoding.

    Returns
    ------------
    decoded : str
      Data as a string
    """
    # if not bytes just return input
    if not hasattr(text, 'decode'):
        return text
    try:
        # initially guess file is UTF-8 or specified encoding
        text = text.decode(initial)
    except UnicodeDecodeError:
        # detect different file encodings
        import chardet
        # try to detect the encoding of the file
        # only look at the first 1000 characters otherwise
        # for big files chardet looks at everything and is slow
        detect = chardet.detect(text[:1000])
        # warn on files that aren't UTF-8
        log.debug(
            'Data not {}! Trying {} (confidence {})'.format(
                initial,
                detect['encoding'],
                detect['confidence']))
        # try to decode again, unwrap in try
        text = text.decode(detect['encoding'], errors='ignore')
    return text


def to_ascii(text):
    """
    Force a string or other to ASCII text ignoring errors.

    Parameters
    -----------
    text : any
      Input to be converted to ASCII string

    Returns
    -----------
    ascii : str
      Input as an ASCII string
    """
    if hasattr(text, 'encode'):
        # case for existing strings
        return text.encode(
            'ascii', errors='ignore').decode('ascii')
    elif hasattr(text, 'decode'):
        # case for bytes
        return text.decode('ascii', errors='ignore')
    # otherwise just wrap as a string
    return str(text)


def is_ccw(points, return_all=False):
    """
    Check if connected 2D points are counterclockwise.

    Parameters
    -----------
    points : (n, 2) float
      Connected points on a plane
    return_all : bool
      Return polygon area and centroid or just counter-clockwise.

    Returns
    ----------
    ccw : bool
      True if points are counter-clockwise
    area : float
      Only returned if `return_centroid`
    centroid : (2,) float
      Centroid of the polygon.
    """
    points = np.array(points, dtype=np.float64)

    if len(points.shape) != 2 or points.shape[1] != 2:
        raise ValueError('only defined for `(n, 2)` points')

    # the "shoelace formula"
    product = np.subtract(*(points[:-1, [1, 0]] * points[1:]).T)
    # the area of the polygon
    area = product.sum() / 2.0
    # check the sign of the area
    ccw = area < 0.0

    if not return_all:
        return ccw

    # the centroid of the polygon uses the same formula
    centroid = ((points[:-1] + points[1:]) *
                product.reshape((-1, 1))).sum(axis=0) / (6.0 * area)

    return ccw, area, centroid


def unique_name(start, contains, counts=None):
    """
    Deterministically generate a unique name not
    contained in a dict, set or other grouping with
    `__includes__` defined. Will create names of the
    form "start_10" and increment accordingly.

    Parameters
    -----------
    start : str
      Initial guess for name.
    contains : dict, set, or list
      Bundle of existing names we can *not* use.
    counts : None or dict
      Maps name starts encountered before to increments in
      order to speed up finding a unique name as otherwise
      it potentially has to iterate through all of contains.
      Should map to "how many times has this `start`
      been attempted, i.e. `counts[start]: int`.
      Note that this *will be mutated* in-place by this function!

    Returns
    ---------
    unique : str
      A name that is not contained in `contains`
    """
    # exit early if name is not in bundle
    if (start is not None and
        len(start) > 0 and
            start not in contains):
        return start

    # start checking with zero index unless found
    if counts is None:
        increment = 0
    else:
        increment = counts.get(start, 0)
    if start is not None and len(start) > 0:
        formatter = start + '_{}'
        # split by our delimiter once
        split = start.rsplit('_', 1)
        if len(split) == 2 and increment == 0:
            try:
                # start incrementing from the existing
                # trailing value
                # if it is not an integer this will fail
                increment = int(split[1])
                # include the first split value
                formatter = split[0] + '_{}'
            except BaseException:
                pass
    else:
        formatter = 'geometry_{}'

    # if contains is empty we will only need to check once
    for i in range(increment + 1, 2 + increment + len(contains)):
        check = formatter.format(i)
        if check not in contains:
            if counts is not None:
                counts[start] = i
            return check

    # this should really never happen since we looped
    # through the full length of contains
    raise ValueError('Unable to establish unique name!')


# trimesh/version.py

__version__ = '3.22.5'

if __name__ == '__main__':
    # print version if run directly i.e. in a CI script
    print(__version__)  # noqa


# trimesh/visual/__init__.py

# flake8: NOQA
"""
visual
-------------

Handle visual properties for meshes, including color and texture
"""

from .color import (ColorVisuals,
                    random_color,
                    to_rgba,
                    DEFAULT_COLOR,
                    interpolate,
                    uv_to_color,
                    uv_to_interpolated_color,
                    linear_color_map)

from .texture import TextureVisuals
from .objects import create_visual, concatenate

from . import color
from . import texture
from . import objects
from . import material
from .. import resolvers


# explicitly list imports in __all__
# as otherwise flake8 gets mad
__all__ = ['color',
           'texture',
           'resolvers',
           'TextureVisuals',
           'ColorVisuals',
           'random_color',
           'to_rgba',
           'create_visual',
           'DEFAULT_COLOR',
           'interpolate',
           'linear_color_map',
           'uv_to_color',
           'uv_to_interpolated_color']


# trimesh/visual/base.py

"""
base.py
-------------

The base class for `Visual` objects
"""
import abc
from ..util import ABC


class Visuals(ABC):
    """
    Parent of Visual classes.
    """
    @abc.abstractproperty
    def kind(self):
        pass

    @abc.abstractmethod
    def update_vertices(self, mask):
        pass

    @abc.abstractmethod
    def update_faces(self, mask):
        pass

    @abc.abstractmethod
    def concatenate(self, other):
        pass

    @abc.abstractmethod
    def __hash__(self):
        pass

    @abc.abstractmethod
    def copy(self):
        pass

    def __add__(self, other):
        """
        Concatenate two ColorVisuals objects into a single object.

        Parameters
        -----------
        other : Visuals
          Other visual to concatenate

        Returns
        -----------
        result : Visuals
          Object containing information from current
          object and other in the order (self, other)
        """
        return self.concatenate(other)


# trimesh/visual/color.py

"""
color.py
-------------

Hold and deal with visual information about meshes.

There are lots of ways to encode visual information, and the goal of this
architecture is to make it possible to define one, and then transparently
get the others. The two general categories are:

1) colors, defined for a face, vertex, or material
2) textures, defined as an image and UV coordinates for each vertex

This module only implements diffuse colors at the moment.

Goals
----------
1) If nothing is defined sane defaults should be returned
2) If a user alters or sets a value, that is considered user data
   and should be saved and treated as such.
3) Only one 'mode' of visual (vertex or face) is allowed at a time
   and setting or altering a value should automatically change the mode.
"""

import numpy as np

import copy
import colorsys

from .base import Visuals

from .. import util
from .. import caching

from ..grouping import unique_rows
from ..constants import tol


class ColorVisuals(Visuals):
    """
    Store color information about a mesh.
    """

    def __init__(self,
                 mesh=None,
                 face_colors=None,
                 vertex_colors=None):
        """
        Store color information about a mesh.

        Parameters
        ----------
        mesh : Trimesh
          Object that these visual properties
          are associated with
        face_ colors :  (n,3|4) or (3,) or (4,) uint8
          Colors per-face
        vertex_colors : (n,3|4) or (3,) or (4,) uint8
          Colors per-vertex
        """
        self.mesh = mesh
        self._data = caching.DataStore()
        self._cache = caching.Cache(
            id_function=self._data.__hash__)

        self.defaults = {
            'material_diffuse': np.array([102, 102, 102, 255],
                                         dtype=np.uint8),
            'material_ambient': np.array([64, 64, 64, 255],
                                         dtype=np.uint8),
            'material_specular': np.array([197, 197, 197, 255],
                                          dtype=np.uint8),
            'material_shine': 77.0}

        try:
            if face_colors is not None:
                self.face_colors = face_colors
            if vertex_colors is not None:
                self.vertex_colors = vertex_colors
        except ValueError:
            util.log.warning('unable to convert colors!')

    @caching.cache_decorator
    def transparency(self):
        """
        Does the current object contain any transparency.

        Returns
        ----------
        transparency: bool, does the current visual contain transparency
        """
        if 'vertex_colors' in self._data:
            a_min = self._data['vertex_colors'][:, 3].min()
        elif 'face_colors' in self._data:
            a_min = self._data['face_colors'][:, 3].min()
        else:
            return False

        return bool(a_min < 255)

    @property
    def defined(self):
        """
        Are any colors defined for the current mesh.

        Returns
        ---------
        defined : bool
          Are colors defined or not.
        """
        return self.kind is not None

    @property
    def kind(self):
        """
        What color mode has been set.

        Returns
        ----------
        mode : str or None
          One of ('face', 'vertex', None)
        """
        # if nothing is stored anywhere it's a safe bet mode is None
        if not (len(self._cache.cache) > 0 or len(self._data.data) > 0):
            return None

        # do bookkeeping
        self._verify_hash()

        # check modes in data
        if 'vertex_colors' in self._data:
            return 'vertex'
        elif 'face_colors' in self._data:
            return 'face'

        return None

    def __hash__(self):
        return self._data.__hash__()

    def copy(self):
        """
        Return a copy of the current ColorVisuals object.


        Returns
        ----------
        copied : ColorVisuals
          Contains the same information as self
        """
        copied = ColorVisuals()
        # call the literally insane generators
        self.face_colors
        self.vertex_colors
        # copy anything that's actually data
        copied._data.data = copy.deepcopy(self._data.data)

        return copied

    @property
    def face_colors(self):
        """
        Colors defined for each face of a mesh.

        If no colors are defined, defaults are returned.

        Returns
        ----------
        colors : (len(mesh.faces), 4) uint8
          RGBA color for each face
        """
        return self._get_colors(name='face')

    @face_colors.setter
    def face_colors(self, values):
        """
        Set the colors for each face of a mesh.

        This will apply these colors and delete any previously specified
        color information.

        Parameters
        ------------
        colors : (len(mesh.faces), 3), set each face to the specified color
                 (len(mesh.faces), 4), set each face to the specified color
                 (3,) int, set the whole mesh this color
                 (4,) int, set the whole mesh this color
        """
        if values is None:
            if 'face_colors' in self._data:
                self._data.data.pop('face_colors')
            return

        colors = to_rgba(values)

        if (self.mesh is not None and
                colors.shape == (4,)):
            count = len(self.mesh.faces)
            colors = np.tile(colors, (count, 1))

        # if we set any color information, clear the others
        self._data.clear()
        self._data['face_colors'] = colors
        self._cache.verify()

    @property
    def vertex_colors(self):
        """
        Return the colors for each vertex of a mesh

        Returns
        ------------
        colors: (len(mesh.vertices), 4) uint8, color for each vertex
        """
        return self._get_colors(name='vertex')

    @vertex_colors.setter
    def vertex_colors(self, values):
        """
        Set the colors for each vertex of a mesh

        This will apply these colors and delete any previously specified
        color information.

        Parameters
        ------------
        colors : (len(mesh.vertices), 3), set each face to the color
                 (len(mesh.vertices), 4), set each face to the color
                 (3,) int, set the whole mesh this color
                 (4,) int, set the whole mesh this color
        """
        if values is None:
            if 'vertex_colors' in self._data:
                self._data.data.pop('vertex_colors')
            return

        # make sure passed values are numpy array
        values = np.asanyarray(values)
        # Ensure the color shape is sane
        if (self.mesh is not None and not
                (values.shape == (len(self.mesh.vertices), 3) or
                 values.shape == (len(self.mesh.vertices), 4) or
                 values.shape == (3,) or
                 values.shape == (4,))):
            return

        colors = to_rgba(values)
        if (self.mesh is not None and
                colors.shape == (4,)):
            count = len(self.mesh.vertices)
            colors = np.tile(colors, (count, 1))

        # if we set any color information, clear the others
        self._data.clear()
        self._data['vertex_colors'] = colors
        self._cache.verify()

    def _get_colors(self, name):
        """
        A magical function which maintains the sanity of vertex and face colors.

        * If colors have been explicitly stored or changed, they are considered
        user data, stored in self._data (DataStore), and are returned immediately
        when requested.
        * If colors have never been set, a (count,4) tiled copy of the default diffuse
        color will be stored in the cache
        ** the hash on creation for these cached default colors will also be stored
        ** if the cached color array is altered (different hash than when it was
        created) we consider that now to be user data and the array is moved from
        the cache to the DataStore.

        Parameters
        -----------
        name : str
          Values 'face' or 'vertex'

        Returns
        -----------
        colors : (count, 4) uint8
          RGBA colors
        """

        count = None
        try:
            if name == 'face':
                count = len(self.mesh.faces)
            elif name == 'vertex':
                count = len(self.mesh.vertices)
        except BaseException:
            pass

        # the face or vertex colors
        key_colors = str(name) + '_colors'
        # the initial hash of the colors
        key_hash = key_colors + '_hash'

        if key_colors in self._data:
            # if a user has explicitly stored or changed the color it
            # will be in data
            return self._data[key_colors]

        elif key_colors in self._cache:
            # if the colors have been autogenerated already they
            # will be in the cache
            colors = self._cache[key_colors]
            # if the cached colors have been changed since creation we move
            # them to data
            if hash(colors) != self._cache[key_hash]:
                # call the setter on the property using exec
                # this avoids having to pass a setter to this function
                if name == 'face':
                    self.face_colors = colors
                elif name == 'vertex':
                    self.vertex_colors = colors
                else:
                    raise ValueError('unsupported name!!!')
                self._cache.verify()
                # return the stored copy of the colors
                return self._data[key_colors]
        else:
            # colors have never been accessed
            if self.kind is None:
                # no colors are defined, so create a (count, 4) tiled
                # copy of the default color
                colors = np.tile(self.defaults['material_diffuse'],
                                 (count, 1))
            elif (self.kind == 'vertex' and
                  name == 'face'):
                colors = vertex_to_face_color(
                    vertex_colors=self.vertex_colors,
                    faces=self.mesh.faces)
            elif (self.kind == 'face' and
                  name == 'vertex'):
                colors = face_to_vertex_color(
                    mesh=self.mesh,
                    face_colors=self.face_colors)
            else:
                raise ValueError('self.kind not accepted values!!')

        if (count is not None and
                colors.shape != (count, 4)):
            raise ValueError('face colors incorrect shape!')

        # subclass the array to track for changes using a hash
        colors = caching.tracked_array(colors)
        # put the generated colors and their initial checksum into cache
        self._cache[key_colors] = colors
        self._cache[key_hash] = hash(colors)

        return colors

    def _verify_hash(self):
        """
        Verify the checksums of cached face and vertex color, to verify
        that a user hasn't altered them since they were generated from
        defaults.

        If the colors have been altered since creation, move them into
        the DataStore at self._data since the user action has made them
        user data.
        """
        if not hasattr(self, '_cache') or len(self._cache) == 0:
            return

        for name in ['face', 'vertex']:
            # the face or vertex colors
            key_colors = str(name) + '_colors'
            # the initial hash of the colors
            key_hash = key_colors + '_hash'

            if key_colors not in self._cache:
                continue

            colors = self._cache[key_colors]
            # if the cached colors have been changed since creation
            # move them to data
            if hash(colors) != self._cache[key_hash]:
                if name == 'face':
                    self.face_colors = colors
                elif name == 'vertex':
                    self.vertex_colors = colors
                else:
                    raise ValueError('unsupported name!!!')
                self._cache.verify()

    def update_vertices(self, mask):
        """
        Apply a mask to remove or duplicate vertex properties.
        """
        self._update_key(mask, 'vertex_colors')

    def update_faces(self, mask):
        """
        Apply a mask to remove or duplicate face properties
        """
        self._update_key(mask, 'face_colors')

    def face_subset(self, face_index):
        """
        Given a mask of face indices, return a sliced version.

        Parameters
        ----------
        face_index: (n,) int, mask for faces
                    (n,) bool, mask for faces

        Returns
        ----------
        visual: ColorVisuals object containing a subset of faces.
        """
        kwargs = {}
        if self.defined:
            if self.face_colors is not None:
                kwargs.update(face_colors=self.face_colors[face_index])

            if self.vertex_colors is not None:
                indices = np.unique(self.mesh.faces[face_index].flatten())
                vertex_colors = self.vertex_colors[indices]
                kwargs.update(vertex_colors=vertex_colors)

        result = ColorVisuals(**kwargs)

        return result

    @property
    def main_color(self):
        """
        What is the most commonly occurring color.

        Returns
        ------------
        color: (4,) uint8, most common color
        """
        if self.kind is None:
            return DEFAULT_COLOR
        elif self.kind == 'face':
            colors = self.face_colors
        elif self.kind == 'vertex':
            colors = self.vertex_colors
        else:
            raise ValueError('color kind incorrect!')

        # find the unique colors
        unique, inverse = unique_rows(colors)
        # the most commonly occurring color, or mode
        # this will be an index of inverse, not colors
        mode_index = np.bincount(inverse).argmax()
        color = colors[unique[mode_index]]

        return color

    def to_texture(self):
        """
        Convert the current ColorVisuals object to a texture
        with a `SimpleMaterial` defined.

        Returns
        ------------
        visual : trimesh.visual.TextureVisuals
          Copy of the current visuals as a texture.
        """
        from .texture import TextureVisuals
        mat, uv = color_to_uv(vertex_colors=self.vertex_colors)
        return TextureVisuals(material=mat, uv=uv)

    def concatenate(self, other, *args):
        """
        Concatenate two or more ColorVisuals objects
        into a single object.

        Parameters
        -----------
        other : ColorVisuals
          Object to append
        *args: ColorVisuals objects

        Returns
        -----------
        result : ColorVisuals
          Containing information from current
          object and others in the order it was passed.
        """
        # avoid a circular import
        from . import objects
        result = objects.concatenate(self, other, *args)
        return result

    def _update_key(self, mask, key):
        """
        Mask the value contained in the DataStore at a specified key.

        Parameters
        -----------
        mask: (n,) int
              (n,) bool
        key: hashable object, in self._data
        """
        mask = np.asanyarray(mask)
        if key in self._data:
            self._data[key] = self._data[key][mask]


class VertexColor(Visuals):
    """
    Create a simple visual object to hold just vertex colors
    for objects such as PointClouds.
    """

    def __init__(self, colors=None, obj=None):
        """
        Create a vertex color visual
        """
        self.obj = obj
        self.vertex_colors = colors

    @property
    def kind(self):
        return 'vertex'

    def update_vertices(self, mask):
        if self._colors is not None:
            self._colors = self._colors[mask]

    def update_faces(self, mask):
        pass

    @property
    def vertex_colors(self):
        return self._colors

    @vertex_colors.setter
    def vertex_colors(self, data):
        if data is None:
            self._colors = caching.tracked_array(None)
        else:
            # tile single color into color array
            data = np.asanyarray(data)
            if data.shape in [(3,), (4,)]:
                data = np.tile(data, (len(self.obj.vertices), 1))
            # track changes in colors and convert to RGBA
            self._colors = caching.tracked_array(to_rgba(data))

    def copy(self):
        """
        Return a copy of the current visuals
        """
        return copy.deepcopy(self)

    def concatenate(self, other):
        """
        Concatenate this visual object with another
        VertexVisuals.

        Parameters
        -----------
        other : VertexColors or ColorVisuals
          Other object to concatenate

        Returns
        ------------
        concate : VertexColor
          Object with both colors
        """
        return VertexColor(colors=np.vstack(
            self.vertex_colors,
            other.vertex_colors))

    def __hash__(self):
        return self._colors.__hash__()


def to_rgba(colors, dtype=np.uint8):
    """
    Convert a single or multiple RGB colors to RGBA colors.

    Parameters
    ----------
    colors : (n, 3) or (n, 4) array
      RGB or RGBA colors

    Returns
    ----------
    colors : (n, 4) list of RGBA colors
             (4,)  single RGBA color
    """
    if colors is None:
        return DEFAULT_COLOR

    # colors as numpy array
    colors = np.asanyarray(colors)

    # integer value for opaque alpha given our datatype
    opaque = np.iinfo(dtype).max

    if colors.dtype.kind == 'f':
        # replace any `nan` or `inf` values with zero
        colors[~np.isfinite(colors)] = 0.0

    if (colors.dtype.kind == 'f' and colors.max() < (1.0 + 1e-8)):
        colors = (colors * opaque).round().astype(dtype)
    elif (colors.max() <= opaque):
        colors = colors.astype(dtype)
    else:
        raise ValueError('colors non-convertible!')

    if util.is_shape(colors, (-1, 3)):
        # add an opaque alpha for RGB colors
        colors = np.column_stack((
            colors,
            opaque * np.ones(len(colors)))).astype(dtype)
    elif util.is_shape(colors, (3,)):
        # if passed a single RGB color add an alpha
        colors = np.append(colors, opaque).astype(dtype)

    if not (util.is_shape(colors, (4,)) or
            util.is_shape(colors, (-1, 4))):
        raise ValueError('Colors not of appropriate shape!')

    return colors


def to_float(colors):
    """
    Convert integer colors to 0.0 - 1.0 floating point colors

    Parameters
    -------------
    colors : (n, d) int
      Integer colors

    Returns
    -------------
    as_float : (n, d) float
      Float colors 0.0 - 1.0
    """

    # colors as numpy array
    colors = np.asanyarray(colors)
    if colors.dtype.kind == 'f':
        return colors
    elif colors.dtype.kind in 'iu':
        # integer value for opaque alpha given our datatype
        opaque = np.iinfo(colors.dtype).max
        return colors.astype(np.float64) / opaque
    else:
        raise ValueError('only works on int or float colors!')


def hex_to_rgba(color):
    """
    Turn a string hex color to a (4,) RGBA color.

    Parameters
    -----------
    color: str, hex color

    Returns
    -----------
    rgba: (4,) np.uint8, RGBA color
    """
    value = str(color).lstrip('#').strip()
    if len(value) == 6:
        rgb = [int(value[i:i + 2], 16) for i in (0, 2, 4)]
        rgba = np.append(rgb, 255).astype(np.uint8)
    else:
        raise ValueError('Only RGB supported')

    return rgba


def random_color(dtype=np.uint8):
    """
    Return a random RGB color using datatype specified.

    Parameters
    ----------
    dtype: numpy dtype of result

    Returns
    ----------
    color: (4,) dtype, random color that looks OK
    """
    hue = np.random.random() + .61803
    hue %= 1.0
    color = np.array(colorsys.hsv_to_rgb(hue, .99, .99))
    if np.dtype(dtype).kind in 'iu':
        max_value = (2**(np.dtype(dtype).itemsize * 8)) - 1
        color *= max_value
    color = np.append(color, max_value).astype(dtype)
    return color


def vertex_to_face_color(vertex_colors, faces):
    """
    Convert a list of vertex colors to face colors.

    Parameters
    ----------
    vertex_colors: (n,(3,4)),  colors
    faces:         (m,3) int, face indexes

    Returns
    -----------
    face_colors: (m,4) colors
    """
    vertex_colors = to_rgba(vertex_colors)
    face_colors = vertex_colors[faces].mean(axis=1)
    return face_colors.astype(np.uint8)


def face_to_vertex_color(
        mesh,
        face_colors,
        dtype=np.uint8):
    """
    Convert face colors into vertex colors.

    Parameters
    -----------
    mesh : trimesh.Trimesh object
    face_colors: (n, (3,4)) int, face colors
    dtype:       data type of output

    Returns
    -----------
    vertex_colors: (m,4) dtype, colors for each vertex
    """
    rgba = to_rgba(face_colors)
    vertex = mesh.faces_sparse.dot(rgba.astype(np.float64))
    degree = mesh.vertex_degree

    # normalize color by the number of faces including
    # the vertex (i.e. the vertex degree)
    nonzero = degree > 0
    vertex[nonzero] /= degree[nonzero].reshape((-1, 1))

    assert vertex.shape == (len(mesh.vertices), 4)

    return vertex.astype(dtype)


def colors_to_materials(colors, count=None):
    """
    Convert a list of colors into a list of unique materials
    and material indexes.

    Parameters
    -----------
    colors : (n, 3) or (n, 4) float
      RGB or RGBA colors
    count : int
      Number of entities to apply color to

    Returns
    -----------
    diffuse : (m, 4) int
      Colors
    index : (count,) int
      Index of each color
    """

    # convert RGB to RGBA
    rgba = to_rgba(colors)

    # if we were only passed a single color
    if util.is_shape(rgba, (4,)) and count is not None:
        diffuse = rgba.reshape((-1, 4))
        index = np.zeros(count, dtype=np.int64)
    elif util.is_shape(rgba, (-1, 4)):
        # we were passed multiple colors
        # find the unique colors in the list to save as materials
        unique, index = unique_rows(rgba)
        diffuse = rgba[unique]
    else:
        raise ValueError('Colors not convertible!')

    return diffuse, index


def linear_color_map(values, color_range=None):
    """
    Linearly interpolate between two colors.

    If colors are not specified the function will
    interpolate between  0.0 values as red and 1.0 as green.

    Parameters
    --------------
    values : (n, ) float
      Values to interpolate
    color_range : None, or (2, 4) uint8
      What colors should extrema be set to

    Returns
    ---------------
    colors : (n, 4) uint8
      RGBA colors for interpolated values
    """

    if color_range is None:
        color_range = np.array([[255, 0, 0, 255],
                                [0, 255, 0, 255]],
                               dtype=np.uint8)
    else:
        color_range = np.asanyarray(color_range,
                                    dtype=np.uint8)

    if color_range.shape != (2, 4):
        raise ValueError('color_range must be RGBA (2, 4)')

    # float 1D array clamped to 0.0 - 1.0
    values = np.clip(np.asanyarray(
        values, dtype=np.float64).ravel(),
        0.0, 1.0).reshape((-1, 1))

    # the stacked component colors
    color = [np.ones((len(values), 4)) * c
             for c in color_range.astype(np.float64)]

    # interpolated colors
    colors = (color[1] * values) + (color[0] * (1.0 - values))

    # rounded and set to correct data type
    colors = np.round(colors).astype(np.uint8)

    return colors


def interpolate(values, color_map=None, dtype=np.uint8):
    """
    Given a 1D list of values, return interpolated colors
    for the range.

    Parameters
    ---------------
    values : (n, ) float
      Values to be interpolated over
    color_map : None, or str
      Key to a colormap contained in:
      matplotlib.pyplot.colormaps()
      e.g: 'viridis'

    Returns
    -------------
    interpolated : (n, 4) dtype
      Interpolated RGBA colors
    """

    # get a color interpolation function
    if color_map is None:
        cmap = linear_color_map
    else:
        from matplotlib.pyplot import get_cmap
        cmap = get_cmap(color_map)

    # make input always float
    values = np.asanyarray(values, dtype=np.float64).ravel()
    # scale values to 0.0 - 1.0 and get colors
    colors = cmap((values - values.min()) / values.ptp())
    # convert to 0-255 RGBA
    rgba = to_rgba(colors, dtype=dtype)

    return rgba


def uv_to_color(uv, image):
    """
    Get the color in a texture image.

    Parameters
    -------------
    uv : (n, 2) float
      UV coordinates on texture image
    image : PIL.Image
      Texture image

    Returns
    ----------
    colors : (n, 4) uint4
      RGBA color at each of the UV coordinates
    """
    if image is None or uv is None:
        return None

    # UV coordinates should be (n, 2) float
    uv = np.asanyarray(uv, dtype=np.float64) % 1.0

    # get texture image pixel positions of UV coordinates
    x = (uv[:, 0] * (image.width - 1))
    y = ((1 - uv[:, 1]) * (image.height - 1))

    # access colors from pixel locations
    # make sure image is RGBA before getting values
    colors = np.asanyarray(image.convert('RGBA'))[
        y.round().astype(np.int64), x.round().astype(np.int64)]

    # conversion to RGBA should have corrected shape
    assert colors.ndim == 2 and colors.shape[1] == 4
    assert colors.dtype == np.uint8

    return colors


def uv_to_interpolated_color(uv, image):
    """
    Get the color from texture image using bilinear sampling.

    Parameters
    -------------
    uv : (n, 2) float
      UV coordinates on texture image
    image : PIL.Image
      Texture image

    Returns
    ----------
    colors : (n, 4) uint8
      RGBA color at each of the UV coordinates.
    """
    if image is None or uv is None:
        return None

    # UV coordinates should be (n, 2) float
    uv = np.asanyarray(uv, dtype=np.float64)

    # get texture image pixel positions of UV coordinates
    x = (uv[:, 0] * (image.width - 1))
    y = ((1 - uv[:, 1]) * (image.height - 1))

    x_floor = np.floor(x).astype(np.int64) % image.width
    y_floor = np.floor(y).astype(np.int64) % image.height

    x_ceil = np.ceil(x).astype(np.int64) % image.width
    y_ceil = np.ceil(y).astype(np.int64) % image.height

    dx = x % image.width - x_floor
    dy = y % image.height - y_floor

    img = np.asanyarray(image.convert('RGBA'))

    colors00 = img[y_floor, x_floor]
    colors01 = img[y_ceil, x_floor]
    colors10 = img[y_floor, x_ceil]
    colors11 = img[y_ceil, x_ceil]

    a00 = (1 - dx) * (1 - dy)
    a01 = dx * (1 - dy)
    a10 = (1 - dx) * dy
    a11 = dx * dy

    a00 = np.repeat(a00[:, None], 4, axis=1)
    a01 = np.repeat(a01[:, None], 4, axis=1)
    a10 = np.repeat(a10[:, None], 4, axis=1)
    a11 = np.repeat(a11[:, None], 4, axis=1)

    # interpolated colors as floating point then convert back to uint8
    colors = (a00 * colors00 + a01 * colors01 +
              a10 * colors10 + a11 * colors11).round().astype(np.uint8)

    # conversion to RGBA should have corrected shape
    assert colors.ndim == 2 and colors.shape[1] == 4
    assert colors.dtype == np.uint8

    return colors


def color_to_uv(vertex_colors):
    """
    Pack vertex colors into UV coordinates and a simple image material

    Parameters
    ------------
    vertex_colors : (n, 4) float
      Array of vertex colors.

    Returns
    ------------
    material : SimpleMaterial
      Material containing color information.
    uv : (n, 2) float
      Normalized UV coordinates
    """
    from .material import SimpleMaterial, empty_material

    # deduplicate the vertex colors
    unique, inverse = unique_rows(vertex_colors)

    # if there is only one color return a
    if len(unique) == 1:
        # return a simple single-pixel material
        material = empty_material(color=vertex_colors[unique[0]])
        uvs = np.zeros((len(vertex_colors), 2)) + 0.5
        return material, uvs

    from PIL import Image

    # return a square image of (size, size)
    size = int(np.ceil(np.sqrt(len(unique))))
    ctype = vertex_colors.shape[1]

    colors = np.zeros((size ** 2, ctype), dtype=vertex_colors.dtype)
    colors[:len(unique)] = vertex_colors[unique]

    # PIL has reversed x-y coordinates
    image = Image.fromarray(colors.reshape((size, size, ctype))[::-1])

    pos = np.arange(len(unique))
    # create tiled coordinates for the color pixels
    coords = np.column_stack((pos % size, np.floor(pos / size)))

    # normalize the index coords into 0.0 - 1.0
    # and offset them to be centered on the pixel
    coords = (coords / size) + (1.0 / (size * 2.0))
    uvs = coords[inverse]

    if tol.strict:
        # check the packed colors against the image
        check = uv_to_color(image=image, uv=uvs)
        assert np.all(check == vertex_colors)

    return SimpleMaterial(image=image), uvs


# set an arbitrary grey as the default color
DEFAULT_COLOR = np.array([102, 102, 102, 255], dtype=np.uint8)


# trimesh/visual/material.py

"""
material.py
-------------

Store visual materials as objects.
"""
import abc
import copy
import numpy as np

from . import color
from .. import util
from .. import exceptions

from ..constants import tol

# epsilon for comparing floating point
_eps = 1e-5


class Material(util.ABC):
    def __init__(self, *args, **kwargs):
        raise NotImplementedError('must be subclassed!')

    @abc.abstractmethod
    def __hash__(self):
        raise NotImplementedError('must be subclassed!')

    @abc.abstractproperty
    def main_color(self):
        """
        The "average" color of this material.

        Returns
        ---------
        color : (4,) uint8
          Average color of this material.
        """

    @property
    def name(self):
        if hasattr(self, '_name'):
            return self._name
        return 'material_0'

    @name.setter
    def name(self, value):
        self._name = value

    def copy(self):
        return copy.deepcopy(self)


class SimpleMaterial(Material):
    """
    Hold a single image texture.
    """

    def __init__(self,
                 image=None,
                 diffuse=None,
                 ambient=None,
                 specular=None,
                 glossiness=None,
                 **kwargs):

        # save image
        self.image = image

        # save material colors as RGBA
        self.ambient = color.to_rgba(ambient)
        self.diffuse = color.to_rgba(diffuse)
        self.specular = color.to_rgba(specular)

        # save Ns
        self.glossiness = glossiness

        # save other keyword arguments
        self.kwargs = kwargs

    def to_color(self, uv):
        return color.uv_to_color(uv, self.image)

    def to_obj(self, name=None):
        """
        Convert the current material to an OBJ format
        material.

        Parameters
        -----------
        name : str or None
          Name to apply to the material

        Returns
        -----------
        tex_name : str
          Name of material
        mtl_name : str
          Name of mtl file in files
        files : dict
          Data as {file name : bytes}
        """
        # material parameters as 0.0-1.0 RGB
        Ka = color.to_float(self.ambient)[:3]
        Kd = color.to_float(self.diffuse)[:3]
        Ks = color.to_float(self.specular)[:3]

        if name is None:
            name = self.name

        # create an MTL file
        mtl = ['newmtl {}'.format(name),
               'Ka {:0.8f} {:0.8f} {:0.8f}'.format(*Ka),
               'Kd {:0.8f} {:0.8f} {:0.8f}'.format(*Kd),
               'Ks {:0.8f} {:0.8f} {:0.8f}'.format(*Ks),
               'Ns {:0.8f}'.format(self.glossiness)]

        # collect the OBJ data into files
        data = {}

        if self.image is not None:
            image_type = self.image.format
            # what is the name of the export image to save
            if image_type is None:
                image_type = 'png'
            image_name = '{}.{}'.format(name, image_type.lower())
            # save the reference to the image
            mtl.append('map_Kd {}'.format(image_name))

            # save the image texture as bytes in the original format
            f_obj = util.BytesIO()
            self.image.save(fp=f_obj, format=image_type)
            f_obj.seek(0)
            data[image_name] = f_obj.read()

        data['{}.mtl'.format(name)] = '\n'.join(mtl).encode('utf-8')

        return data, name

    def __hash__(self):
        """
        Provide a hash of the material so we can detect
        duplicates.

        Returns
        ------------
        hash : int
          Hash of image and parameters
        """
        if hasattr(self.image, 'tobytes'):
            # start with hash of raw image bytes
            hashed = hash(self.image.tobytes())
        else:
            # otherwise start with zero
            hashed = 0
        # we will add additional parameters with
        # an in-place xor of the additional value
        # if stored as numpy arrays add parameters
        if hasattr(self.ambient, 'tobytes'):
            hashed ^= hash(self.ambient.tobytes())
        if hasattr(self.diffuse, 'tobytes'):
            hashed ^= hash(self.diffuse.tobytes())
        if hasattr(self.specular, 'tobytes'):
            hashed ^= hash(self.specular.tobytes())
        if isinstance(self.glossiness, float):
            hashed ^= hash(int(self.glossiness * 1000))
        return hashed

    @property
    def main_color(self):
        """
        Return the most prominent color.
        """
        return self.diffuse

    @property
    def glossiness(self):
        if hasattr(self, '_glossiness'):
            return self._glossiness
        return 1.0

    @glossiness.setter
    def glossiness(self, value):
        if value is None:
            return
        self._glossiness = float(value)

    def to_pbr(self):
        """
        Convert the current simple material to a
        PBR material.

        Returns
        ------------
        pbr : PBRMaterial
          Contains material information in PBR format.
        """
        # convert specular exponent to roughness
        roughness = (2 / (self.glossiness + 2)) ** (1.0 / 4.0)

        return PBRMaterial(roughnessFactor=roughness,
                           baseColorTexture=self.image,
                           baseColorFactor=self.diffuse)


class MultiMaterial(Material):
    def __init__(self, materials=None, **kwargs):
        """
        Wrapper for a list of Materials.

        Parameters
        ----------
        materials : Optional[List[Material]]
            List of materials with which the container to be initialized.
        """
        if materials is None:
            self.materials = []
        else:
            self.materials = materials

    def to_pbr(self):
        """
        TODO : IMPLEMENT
        """
        pbr = [m for m in self.materials
               if isinstance(m, PBRMaterial)]
        if len(pbr) == 0:
            return PBRMaterial()
        return pbr[0]

    def __hash__(self):
        """
        Provide a hash of the multi material so we can detect
        duplicates.

        Returns
        ------------
        hash : int
          Xor hash of the contained materials.
        """
        hashed = int(np.bitwise_xor.reduce(
            [hash(m) for m in self.materials]))

        return hashed

    def __iter__(self):
        return iter(self.materials)

    def __next__(self):
        return next(self.materials)

    def __len__(self):
        return len(self.materials)

    @property
    def main_color(self):
        """
        The "average" color of this material.

        Returns
        ---------
        color : (4,) uint8
          Average color of this material.
        """

    def add(self, material):
        """
        Adds new material to the container.

        Parameters
        ----------
        material : Material
            The material to be added.
        """
        self.materials.append(material)

    def get(self, idx):
        """
        Get material by index.

        Parameters
        ----------
        idx : int
            Index of the material to be retrieved.

        Returns
        -------
            The material on the given index.
        """
        return self.materials[idx]


class PBRMaterial(Material):
    """
    Create a material for physically based rendering as
    specified by GLTF 2.0:
    https://git.io/fhkPZ

    Parameters with `Texture` in them must be PIL.Image objects
    """

    def __init__(self,
                 name=None,
                 emissiveFactor=None,
                 emissiveTexture=None,
                 baseColorFactor=None,
                 metallicFactor=None,
                 roughnessFactor=None,
                 normalTexture=None,
                 occlusionTexture=None,
                 baseColorTexture=None,
                 metallicRoughnessTexture=None,
                 doubleSided=False,
                 alphaMode=None,
                 alphaCutoff=None,
                 **kwargs):

        # store values in an internal dict
        self._data = {}

        # (3,) float
        self.emissiveFactor = emissiveFactor
        # (3,) or (4,) float with RGBA colors
        self.baseColorFactor = baseColorFactor

        # float
        self.metallicFactor = metallicFactor
        self.roughnessFactor = roughnessFactor
        self.alphaCutoff = alphaCutoff

        # PIL image
        self.normalTexture = normalTexture
        self.emissiveTexture = emissiveTexture
        self.occlusionTexture = occlusionTexture
        self.baseColorTexture = baseColorTexture
        self.metallicRoughnessTexture = metallicRoughnessTexture

        # bool
        self.doubleSided = doubleSided

        # str
        self.name = name
        self.alphaMode = alphaMode

        if len(kwargs) > 0:
            util.log.debug(
                'unsupported material keys: {}'.format(
                    ', '.join(kwargs.keys())))

    @property
    def emissiveFactor(self):
        """
        The factors for the emissive color of the material.
        This value defines linear multipliers for the sampled
        texels of the emissive texture.

        Returns
        -----------
        emissiveFactor : (3,) float
           Ech element in the array MUST be greater than
           or equal to 0 and less than or equal to 1.
        """
        return self._data.get('emissiveFactor')

    @emissiveFactor.setter
    def emissiveFactor(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('emissiveFactor', None)
        else:
            # non-None values must be a floating point
            emissive = np.array(value, dtype=np.float64).reshape(3)
            if emissive.min() < -_eps or emissive.max() > (1 + _eps):
                raise ValueError('all factors must be between 0.0-1.0')
            self._data['emissiveFactor'] = emissive

    @property
    def alphaMode(self):
        """
        The material alpha rendering mode enumeration
        specifying the interpretation of the alpha value of
        the base color.

        Returns
        -----------
        alphaMode : str
          One of 'OPAQUE', 'MASK', 'BLEND'
        """
        return self._data.get('alphaMode')

    @alphaMode.setter
    def alphaMode(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('alphaMode', None)
        else:
            # non-None values must be one of three values
            value = str(value).upper().strip()
            if value not in ['OPAQUE', 'MASK', 'BLEND']:
                raise ValueError('incorrect alphaMode: %s', value)
            self._data['alphaMode'] = value

    @property
    def alphaCutoff(self):
        """
        Specifies the cutoff threshold when in MASK alpha mode.
        If the alpha value is greater than or equal to this value
        then it is rendered as fully opaque, otherwise, it is rendered
        as fully transparent. A value greater than 1.0 will render
        the entire material as fully transparent. This value MUST be
        ignored for other alpha modes. When alphaMode is not defined,
        this value MUST NOT be defined.

        Returns
        -----------
        alphaCutoff : float
          Value of cutoff.
        """
        return self._data.get('alphaCutoff')

    @alphaCutoff.setter
    def alphaCutoff(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('alphaCutoff', None)
        else:
            self._data['alphaCutoff'] = float(value)

    @property
    def doubleSided(self):
        """
        Specifies whether the material is double sided.

        Returns
        -----------
        doubleSided : bool
          Specifies whether the material is double sided.
        """
        return self._data.get('doubleSided')

    @doubleSided.setter
    def doubleSided(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('doubleSided', None)
        else:
            self._data['doubleSided'] = bool(value)

    @property
    def metallicFactor(self):
        """
        The factor for the metalness of the material. This value
        defines a linear multiplier for the sampled metalness values
        of the metallic-roughness texture.


        Returns
        -----------
        metallicFactor : float
          How metally is the material
        """
        return self._data.get('metallicFactor')

    @metallicFactor.setter
    def metallicFactor(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('metallicFactor', None)
        else:
            self._data['metallicFactor'] = float(value)

    @property
    def roughnessFactor(self):
        """
        The factor for the roughness of the material. This value
        defines a linear multiplier for the sampled roughness values
        of the metallic-roughness texture.

        Returns
        -----------
        roughnessFactor : float
          Roughness of material.
        """
        return self._data.get('roughnessFactor')

    @roughnessFactor.setter
    def roughnessFactor(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('roughnessFactor', None)
        else:
            self._data['roughnessFactor'] = float(value)

    @property
    def baseColorFactor(self):
        """
        The factors for the base color of the material. This
        value defines linear multipliers for the sampled texels
        of the base color texture.

        Returns
        ---------
        color : (4,) uint8
          RGBA color
        """
        return self._data.get('baseColorFactor')

    @baseColorFactor.setter
    def baseColorFactor(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('baseColorFactor', None)
        else:
            # non-None values must be RGBA color
            self._data['baseColorFactor'] = color.to_rgba(value)

    @property
    def normalTexture(self):
        """
        The normal map texture.

        Returns
        ----------
        image : PIL.Image
          Normal texture.
        """
        return self._data.get('normalTexture')
    
    @normalTexture.setter
    def normalTexture(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('normalTexture', None)
        else:
            self._data['normalTexture'] = value

    @property
    def emissiveTexture(self):
        """
        The emissive texture.

        Returns
        ----------
        image : PIL.Image
          Emissive texture.
        """
        return self._data.get('emissiveTexture')
    
    @emissiveTexture.setter
    def emissiveTexture(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('emissiveTexture', None)
        else:
            self._data['emissiveTexture'] = value

    @property
    def occlusionTexture(self):
        """
        The occlusion texture.

        Returns
        ----------
        image : PIL.Image
          Occlusion texture.
        """
        return self._data.get('occlusionTexture')
    
    @occlusionTexture.setter
    def occlusionTexture(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('occlusionTexture', None)
        else:
            self._data['occlusionTexture'] = value

    @property
    def baseColorTexture(self):
        """
        The base color texture image.

        Returns
        ----------
        image : PIL.Image
          Color texture.
        """
        return self._data.get('baseColorTexture')

    @baseColorTexture.setter
    def baseColorTexture(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('baseColorTexture', None)
        else:
            # non-None values must be RGBA color
            self._data['baseColorTexture'] = value

    @property
    def metallicRoughnessTexture(self):
        """
        The metallic-roughness texture.

        Returns
        ----------
        image : PIL.Image
          Metallic-roughness texture.
        """
        return self._data.get('metallicRoughnessTexture')
    
    @metallicRoughnessTexture.setter
    def metallicRoughnessTexture(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('metallicRoughnessTexture', None)
        else:
            self._data['metallicRoughnessTexture'] = value

    @property
    def name(self):
        return self._data.get('name')
    
    @name.setter
    def name(self, value):
        if value is None:
            # passing none effectively removes value
            self._data.pop('name', None)
        else:
            self._data['name'] = value

    def copy(self):
        # doing a straight deepcopy fails due to PIL images
        kwargs = {}
        # collect stored values as kwargs
        for k, v in self._data.items():
            if v is None:
                continue
            if hasattr(v, 'copy'):
                # use an objects explicit copy if available
                kwargs[k] = v.copy()
            else:
                # otherwise just hope deepcopy does something
                kwargs[k] = copy.deepcopy(v)
        return PBRMaterial(**kwargs)

    def to_color(self, uv):
        """
        Get the rough color at a list of specified UV
        coordinates.

        Parameters
        -------------
        uv : (n, 2) float
          UV coordinates on the material

        Returns
        -------------
        colors
        """
        colors = color.uv_to_color(
            uv=uv, image=self.baseColorTexture)
        if colors is None and self.baseColorFactor is not None:
            colors = self.baseColorFactor.copy()
        return colors

    def to_simple(self):
        """
        Get a copy of the current PBR material as
        a simple material.

        Returns
        ------------
        simple : SimpleMaterial
          Contains material information in a simple manner
        """

        return SimpleMaterial(image=self.baseColorTexture,
                              diffuse=self.baseColorFactor)

    @property
    def main_color(self):
        # will return default color if None
        result = color.to_rgba(self.baseColorFactor)
        return result

    def __hash__(self):
        """
        Provide a hash of the material so we can detect
        duplicate materials.

        Returns
        ------------
        hash : int
          Hash of image and parameters
        """
        return hash(bytes().join(
            np.asanyarray(v).tobytes()
            for v in self._data.values() if v is not None))


def empty_material(color=None):
    """
    Return an empty material set to a single color

    Parameters
    -----------
    color : None or (3,) uint8
      RGB color

    Returns
    -------------
    material : SimpleMaterial
      Image is a a one pixel RGB
    """
    try:
        from PIL import Image
    except BaseException as E:
        return exceptions.ExceptionWrapper(E)

    final = np.array([255, 255, 255, 255], dtype=np.uint8)
    if np.shape(color) in ((3,), (4,)):
        final[:len(color)] = color

    # create a one pixel RGB image
    image = Image.fromarray(final.reshape((1, 1, 4)).astype(np.uint8))
    return SimpleMaterial(image=image)


def pack(materials, uvs, deduplicate=True):
    """
    Pack multiple materials with texture into a single material.

    UV coordinates outside of the 0.0-1.0 range will be coerced
    into this range using a "wrap" behavior (i.e. modulus).

    Parameters
    -----------
    materials : (n,) Material
      List of multiple materials
    uvs : (n, m, 2) float
      Original UV coordinates

    Returns
    ------------
    material : SimpleMaterial
      Combined material.
    uv : (p, 2) float
      Combined UV coordinates in the 0.0-1.0 range.
    """

    from PIL import Image
    from ..path import packing
    import collections

    def material_to_img(mat):
        """
        Logic for extracting a simple image from each material.
        """
        # extract an image for each material
        img = None
        if isinstance(mat, PBRMaterial):
            if mat.baseColorTexture is not None:
                img = mat.baseColorTexture
            elif mat.baseColorFactor is not None:
                c = color.to_rgba(mat.baseColorFactor)
                assert c.shape == (4,)
                assert c.dtype == np.uint8
                img = Image.fromarray(c.reshape((1, 1, -1)))
        elif getattr(mat, 'image', None) is not None:
            img = mat.image
        elif np.shape(getattr(mat, 'diffuse', [])) == (4,):
            # return a one pixel image
            img = Image.fromarray(np.reshape(
                color.to_rgba(mat.diffuse), (1, 1, 4)).astype(np.uint8))

        if img is None:
            # return a one pixel image
            img = Image.fromarray(np.reshape(
                [100, 100, 100, 255], (1, 1, 4)).astype(np.uint8))
        # make sure we're always returning in RGBA mode
        return img.convert('RGBA')

    if deduplicate:
        # start by collecting a list of indexes for each material hash
        unique_idx = collections.defaultdict(list)
        [unique_idx[hash(m)].append(i)
         for i, m in enumerate(materials)]
        # now we only need the indexes and don't care about the hashes
        mat_idx = list(unique_idx.values())
    else:
        # otherwise just use all the indexes
        mat_idx = np.arange(len(materials)).reshape((-1, 1))

    assert set(np.concatenate(mat_idx).ravel()) == set(range(len(uvs)))
    assert len(uvs) == len(materials)

    # collect the images from the materials
    images = [material_to_img(materials[g[0]]) for g in mat_idx]

    # pack the multiple images into a single large image
    final, offsets = packing.images(images, power_resize=True)

    # the size of the final texture image
    final_size = np.array(final.size, dtype=np.float64)
    # collect scaled new UV coordinates by material index
    new_uv = {}
    for group, img, off in zip(mat_idx, images, offsets):
        # how big was the original image
        scale = img.size / final_size
        # what is the offset in fractions of final image
        xy_off = off / final_size
        # scale and translate each of the new UV coordinates
        # also make sure they are in 0.0-1.0 using modulus (i.e. wrap)
        new_uv.update({g: ((uvs[g] % 1.0) * scale) + xy_off
                       for g in group})

    # stack the new UV coordinates in the original order
    stacked = np.vstack([new_uv[i] for i in range(len(uvs))])

    # check to make sure the packed result image matches
    # the original input image exactly in unit tests
    if tol.strict:
        # get the pixel color from the original image
        check = []
        for uv, mat in zip(uvs, materials):
            # get the image from the material and whether or not
            # it had to fill in with default dataa
            img = material_to_img(mat)
            current = color.uv_to_color(image=img, uv=(uv % 1))
            check.append(current)

        check_flat = np.vstack(check)
        # get the pixel color from the packed image
        compare = color.uv_to_color(
            uv=stacked, image=final)
        # should be exactly identical
        # note this is only true for simple colors
        # interpolation on complicated stuff can break this
        assert (compare == check_flat).all()

    return SimpleMaterial(image=final), stacked


# trimesh/visual/objects.py

"""
objects.py
--------------

Deal with objects which hold visual properties, like
ColorVisuals and TextureVisuals.
"""
import numpy as np

from .material import pack
from .texture import TextureVisuals
from .color import ColorVisuals, color_to_uv


def create_visual(**kwargs):
    """
    Create Visuals object from keyword arguments.

    Parameters
    -----------
    face_colors : (n, 3|4) uint8
      Face colors
    vertex_colors : (n, 3|4) uint8
      Vertex colors
    mesh : trimesh.Trimesh
      Mesh object

    Returns
    ----------
    visuals : ColorVisuals
      Visual object created from arguments
    """
    return ColorVisuals(**kwargs)


def concatenate(visuals, *args):
    """
    Concatenate multiple visual objects.

    Parameters
    ----------
    visuals : ColorVisuals or list
      Visuals to concatenate
    *args :  ColorVisuals or list
      More visuals to concatenate

    Returns
    ----------
    concat : Visuals
      If all are color
    """
    # get a flat list of Visuals objects
    if len(args) > 0:
        visuals = np.append(visuals, args)
    else:
        visuals = np.array(visuals)

    # if there are any texture visuals convert all to texture
    if any(v.kind == 'texture' for v in visuals):
        # first collect materials and UV coordinates
        mats = []
        uvs = []
        for v in visuals:
            if v.kind == 'texture':
                mats.append(v.material)
                if v.uv is None:
                    # otherwise use zeros
                    uvs.append(np.zeros((len(v.mesh.vertices), 2)) + 0.5)
                else:
                    # if uvs are of correct shape use them
                    uvs.append(v.uv)

            else:
                # create a material and UV coordinates from vertex colors
                color_mat, color_uv = color_to_uv(
                    vertex_colors=v.vertex_colors)
                mats.append(color_mat)
                uvs.append(color_uv)
        # pack the materials and UV coordinates into one
        new_mat, new_uv = pack(materials=mats, uvs=uvs)
        return TextureVisuals(material=new_mat, uv=new_uv)

    # convert all visuals to the kind of the first
    kind = visuals[0].kind
    if kind == 'face':
        colors = np.vstack([
            v.face_colors for v in visuals])
        return ColorVisuals(face_colors=colors)
    elif kind == 'vertex':
        colors = np.vstack([
            v.vertex_colors for v in visuals])
        return ColorVisuals(vertex_colors=colors)

    return ColorVisuals()


# trimesh/visual/texture.py

import copy

import numpy as np

from .base import Visuals
from . import color

from .. import util
from .. import caching
from .. import grouping

from .material import SimpleMaterial, PBRMaterial, empty_material  # NOQA


class TextureVisuals(Visuals):
    def __init__(self,
                 uv=None,
                 material=None,
                 image=None,
                 face_materials=None):
        """
        Store a single material and per-vertex UV coordinates
        for a mesh.

        If passed UV coordinates and a single image it will
        create a SimpleMaterial for the image.

        Parameters
        --------------
        uv : (n, 2) float
          UV coordinates for the mesh
        material : Material
          Store images and properties
        image : PIL.Image
          Can be passed to automatically create material
        """

        # store values we care about enough to hash
        self.vertex_attributes = caching.DataStore()
        # cache calculated values
        self._cache = caching.Cache(self.vertex_attributes.__hash__)

        # should be (n, 2) float
        self.uv = uv

        if material is None:
            if image is None:
                self.material = empty_material()
            else:
                # if an image is passed create a SimpleMaterial
                self.material = SimpleMaterial(image=image)
        else:
            # if passed assign
            self.material = material

        self.face_materials = face_materials

    def _verify_hash(self):
        """
        Dump the cache if anything in self.vertex_attributes
        has changed.
        """
        self._cache.verify()

    @property
    def kind(self):
        """
        Return the type of visual data stored

        Returns
        ----------
        kind : str
          What type of visuals are defined
        """
        return 'texture'

    @property
    def defined(self):
        """
        Check if any data is stored

        Returns
        ----------
        defined : bool
          Are UV coordinates and images set?
        """
        ok = self.material is not None
        return ok

    def __hash__(self):
        """
        Get a CRC of the stored data.

        Returns
        --------------
        crc : int
          Hash of items in self.vertex_attributes
        """
        return self.vertex_attributes.__hash__()

    @property
    def uv(self):
        """
        Get the stored UV coordinates.

        Returns
        ------------
        uv : (n, 2) float or None
          Pixel position per-vertex.
        """
        return self.vertex_attributes.get('uv', None)

    @uv.setter
    def uv(self, values):
        """
        Set the UV coordinates.

        Parameters
        --------------
        values : (n, 2) float or None
          Pixel locations on a texture per- vertex
        """
        if values is None:
            self.vertex_attributes.pop('uv')
        else:
            self.vertex_attributes['uv'] = np.asanyarray(
                values, dtype=np.float64)

    def copy(self, uv=None):
        """
        Return a copy of the current TextureVisuals object.

        Returns
        ----------
        copied : TextureVisuals
          Contains the same information in a new object
        """
        if uv is None:
            uv = self.uv
        if uv is not None:
            uv = uv.copy()
        copied = TextureVisuals(
            uv=uv,
            material=self.material.copy(),
            face_materials=copy.copy(self.face_materials))

        return copied

    def to_color(self):
        """
        Convert textured visuals to a ColorVisuals with vertex
        color calculated from texture.

        Returns
        -----------
        vis : trimesh.visuals.ColorVisuals
          Contains vertex color from texture
        """
        # find the color at each UV coordinate
        colors = self.material.to_color(self.uv)
        # create ColorVisuals from result
        vis = color.ColorVisuals(vertex_colors=colors)
        return vis

    def face_subset(self, face_index):
        """
        Get a copy of
        """
        if self.uv is not None:
            indices = np.unique(self.mesh.faces[face_index].flatten())
            return self.copy(self.uv[indices])
        else:
            return self.copy()

    def update_vertices(self, mask):
        """
        Apply a mask to remove or duplicate vertex properties.

        Parameters
        ------------
        mask : (len(vertices),) bool or (n,) int
          Mask which can be used like: `vertex_attribute[mask]`
        """
        # collect updated masked values
        updates = {}
        for key, value in self.vertex_attributes.items():
            # DataStore will convert None to zero-length array
            if len(value) == 0:
                continue
            try:
                # store the update
                updates[key] = value[mask]
            except BaseException:
                # usual reason is an incorrect size or index
                util.log.warning('failed to update visual: `{}`'.format(key))
        # clear all values from the vertex attributes
        self.vertex_attributes.clear()
        # apply the updated values
        self.vertex_attributes.update(updates)

    def update_faces(self, mask):
        """
        Apply a mask to remove or duplicate face properties,
        not applicable to texture visuals.
        """

    def concatenate(self, others):
        """
        Concatenate this TextureVisuals object with others
        and return the result without modifying this visual.

        Parameters
        -----------
        others : (n,) Visuals
          Other visual objects to concatenate

        Returns
        -----------
        concatenated : TextureVisuals
          Concatenated visual objects
        """
        from .objects import concatenate
        return concatenate(self, others)


def unmerge_faces(faces, *args, **kwargs):
    """
    Textured meshes can come with faces referencing vertex
    indices (`v`) and an array the same shape which references
    vertex texture indices (`vt`) and sometimes even normal (`vn`).

    Vertex locations with different values of any of these can't
    be considered the "same" vertex, and for our simple data
    model we need to not combine these vertices.

    Parameters
    -------------
    faces : (n, d) int
      References vertex indices
    *args : (n, d) int
      Various references of corresponding values
      This is usually UV coordinates or normal indexes
    maintain_faces : bool
      Do not alter original faces and return no-op masks.

    Returns
    -------------
    new_faces : (m, d) int
      New faces for masked vertices
    mask_v : (p,) int
      A mask to apply to vertices
    mask_* : (p,) int
      A mask to apply to vt array to get matching UV coordinates
      Returns as many of these as args were passed
    """
    # unfortunately Python2 doesn't let us put named kwargs
    # after an `*args` sequence so we have to do this ugly get
    maintain_faces = kwargs.get('maintain_faces', False)

    # don't alter faces
    if maintain_faces:
        # start with not altering faces at all
        result = [faces]
        # find the maximum index referenced by faces
        max_idx = faces.max()
        # add a vertex mask which is just ordered
        result.append(np.arange(max_idx + 1))

        # now given the order is fixed do our best on the rest of the order
        for arg in args:
            # create a mask of the attribute-vertex mapping
            # note that these might conflict since we're not unmerging
            masks = np.zeros((3, max_idx + 1), dtype=np.int64)
            # set the mask using the unmodified face indexes
            for i, f, a in zip(range(3), faces.T, arg.T):
                masks[i][f] = a
            # find the most commonly occurring attribute (i.e. UV coordinate)
            # and use that index note that this is doing a float conversion
            # and then median before converting back to int: could also do this as
            # a column diff and sort but this seemed easier and is fast enough
            result.append(np.median(masks, axis=0).astype(np.int64))

        return result

    # stack into pairs of (vertex index, texture index)
    stackable = [np.asanyarray(faces).reshape(-1)]
    # append multiple args to the correlated stack
    # this is usually UV coordinates (vt) and normals (vn)
    for arg in args:
        stackable.append(np.asanyarray(arg).reshape(-1))

    # unify them into rows of a numpy array
    stack = np.column_stack(stackable)
    # find unique pairs: we're trying to avoid merging
    # vertices that have the same position but different
    # texture coordinates
    unique, inverse = grouping.unique_rows(stack)

    # only take the unique pairs
    pairs = stack[unique]
    # try to maintain original vertex order
    order = pairs[:, 0].argsort()
    # apply the order to the pairs
    pairs = pairs[order]

    # we re-ordered the vertices to try to maintain
    # the original vertex order as much as possible
    # so to reconstruct the faces we need to remap
    remap = np.zeros(len(order), dtype=np.int64)
    remap[order] = np.arange(len(order))

    # the faces are just the inverse with the new order
    new_faces = remap[inverse].reshape((-1, faces.shape[1]))

    # the mask for vertices and masks for other args
    result = [new_faces]
    result.extend(pairs.T)

    return result


def power_resize(image, resample=1, square=False):
    """
    Resize a PIL image so every dimension is a power of two.

    Parameters
    ------------
    image : PIL.Image
      Input image
    resample : int
      Passed to Image.resize
    square : bool
      If True, upsize to a square image

    Returns
    -------------
    resized : PIL.Image
      Input image resized
    """
    # what is the current resolution of the image in pixels
    size = np.array(image.size, dtype=np.int64)
    # what is the resolution of the image upsized to the nearest
    # power of two on each axis: allow rectangular textures
    new_size = (2 ** np.ceil(np.log2(size))).astype(np.int64)

    # make every dimension the largest
    if square:
        new_size = np.ones(2, dtype=np.int64) * new_size.max()

    # if we're not powers of two upsize
    if (size != new_size).any():
        return image.resize(new_size, resample=resample)

    return image.copy()


# trimesh/visual/gloss.py
import numpy as np

from ..constants import log
from ..exceptions import ExceptionWrapper

try:
    from PIL.Image import Image, fromarray
except BaseException as E:
    Image = ExceptionWrapper(E)
    fromarray = ExceptionWrapper(E)


def specular_to_pbr(
        specularFactor=None,
        glossinessFactor=None,
        specularGlossinessTexture=None,
        diffuseTexture=None,
        diffuseFactor=None,
        **kwargs):
    """
    Convert the KHR_materials_pbrSpecularGlossiness to a
    metallicRoughness visual.

    Parameters
    -----------
    specularFactor : list[float]
        Specular color values. Ignored if specularGlossinessTexture
        is present and defaults to [1.0, 1.0, 1.0].
    glossinessFactor : float
        glossiness factor in range [0, 1], Ignored if
        specularGlossinessTexture is present and defaults to 1.0.
    specularGlossinessTexture : PIL.Image
        Texture with 4 color channels. With [0,1,2] representing
        specular RGB and 3 glossiness.
    diffuseTexture : PIL.Image
        Texture with 4 color channels. With [0,1,2] representing diffuse
        RGB and 3 opacity.
    diffuseFactor: float
        Diffuse RGBA color. Ignored if diffuseTexture is present
        Defaults to [1.0, 1.0, 1.0, 1.0].

    Returns
    ----------
    kwargs : dict
      Constructor args for a PBRMaterial object.
      Containing:
        - either baseColorTexture or baseColorFactor
        - either metallicRoughnessTexture or metallicFactor and roughnessFactor
    """
    # based on:
    # https://github.com/KhronosGroup/glTF/blob/89427b26fcac884385a2e6d5803d917ab5d1b04f/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows-bjs/js/babylon.pbrUtilities.js#L33-L64

    if not isinstance(Image, ExceptionWrapper):
        log.debug('unable to convert material without pillow!')
        result = {}
        if isinstance(diffuseTexture, dict):
            result['baseColorTexture'] = diffuseTexture
        if diffuseFactor is not None:
            result['baseColorFactor'] = diffuseFactor
        return result

    dielectric_specular = np.array([0.04, 0.04, 0.04], dtype=np.float32)
    epsilon = 1e-6

    def solve_metallic(diffuse, specular, one_minus_specular_strength):
        if isinstance(specular, float) and specular < dielectric_specular[0]:
            return 0.0

        if len(diffuse.shape) == 2:
            diffuse = diffuse[..., None]
        if len(specular.shape) == 2:
            specular = specular[..., None]

        a = dielectric_specular[0]
        b = diffuse * one_minus_specular_strength / \
            (1.0 - dielectric_specular[0]) + specular - 2.0 * dielectric_specular[0]
        c = dielectric_specular[0] - specular
        D = b * b - 4.0 * a * c
        D = np.clip(D, epsilon, None)
        metallic = np.clip((-b + np.sqrt(D)) / (2.0 * a), 0.0, 1.0)
        if isinstance(metallic, np.ndarray):
            metallic[specular < dielectric_specular[0]] = 0.0
        return metallic

    def get_perceived_brightness(rgb):
        return np.dot(rgb[..., :3], [0.299, 0.587, 0.114])

    def toPIL(img):
        if isinstance(img, Image):
            return img
        if img.dtype == np.float32 or img.dtype == np.float64:
            img = (np.clip(img, 0.0, 1.0) * 255.0).astype(np.uint8)
        return Image.fromarray(img)

    def get_float(val):
        if isinstance(val, float):
            return val
        if isinstance(val, np.ndarray) and len(val.shape) == 1:
            return val[0]
        return val.tolist()

    def get_diffuse(diffuseFactor, diffuseTexture):
        diffuseFactor = diffuseFactor if diffuseFactor is not None else [
            1.0, 1.0, 1.0, 1.0]
        diffuseFactor = np.array(diffuseFactor, dtype=np.float32)

        if diffuseTexture is not None:
            diffuse = np.array(diffuseTexture) / 255.0
            if len(diffuse.shape) == 2:
                diffuse = diffuse[..., None]
            if diffuse.shape[-1] == 1:
                diffuse = diffuse * diffuseFactor
            elif diffuse.shape[-1] == 2:
                alpha = diffuse[..., 1:2]
                diffuse = diffuse[..., :1] * diffuseFactor
                diffuse[..., -1:] *= alpha
            elif diffuse.shape[-1] == diffuseFactor.shape[-1]:
                diffuse = diffuse * diffuseFactor
            elif diffuse.shape[-1] == 3 and diffuseFactor.shape[-1] == 4:
                diffuse = np.concatenate([diffuse, np.ones_like(
                    diffuse[..., :1])], axis=-1) * diffuseFactor
            else:
                log.warning(
                    '`diffuseFactor` and `diffuseTexture` have incompatible shapes: ' +
                    f'{diffuseFactor.shape} and {diffuse.shape}')
        else:
            diffuse = diffuseFactor if diffuseFactor is not None else [1, 1, 1, 1]
            diffuse = np.array(diffuse, dtype=np.float32)
        return diffuse

    def get_specular_glossiness(
            specularFactor,
            glossinessFactor,
            specularGlossinessTexture):
        if specularFactor is None:
            specularFactor = [1.0, 1.0, 1.0]
        specularFactor = np.array(specularFactor, dtype=np.float32)
        if glossinessFactor is None:
            glossinessFactor = 1.0
        glossinessFactor = np.array([glossinessFactor], dtype=np.float32)

        # specularGlossinessTexture should be a texture with 4 channels,
        # 3 sRGB channels for specular and 1 linear channel for glossiness.
        # in practice, it can also have just 1, 2, or 3 channels which are then to
        # be multiplied with the provided factors

        if specularGlossinessTexture is not None:
            specularGlossinessTexture = np.array(specularGlossinessTexture)
            specularTexture, glossinessTexture = None, None

            if (len(specularGlossinessTexture.shape) == 2 or
                    specularGlossinessTexture.shape[-1]) == 1:
                # use the one channel as a multiplier for specular and glossiness
                specularTexture = glossinessTexture = specularGlossinessTexture.reshape(
                    (-1, -1, 1))
            elif specularGlossinessTexture.shape[-1] == 3:
                # all channels are specular, glossiness is only a factor
                specularTexture = specularGlossinessTexture[..., :3]
            elif specularGlossinessTexture.shape[-1] == 2:
                # first channel is specular, last channel is glossiness
                specularTexture = specularGlossinessTexture[..., :1]
                glossinessTexture = specularGlossinessTexture[..., 1:2]
            elif specularGlossinessTexture.shape[-1] == 4:
                # first 3 channels are specular, last channel is glossiness
                specularTexture = specularGlossinessTexture[..., :3]
                glossinessTexture = specularGlossinessTexture[..., 3:]

            if specularTexture is not None:
                # convert into [0,1] range. Does this require conversion of sRGB values?
                specular = specularTexture / 255.0
                specular = specular * specularFactor
            else:
                specular = specularFactor

            if glossinessTexture is not None:
                # convert into [0,1] range. Does this require conversion of sRGB values?
                glossiness = glossinessTexture / 255.0
                glossiness = glossiness * glossinessFactor
            else:
                glossiness = glossinessFactor

            one_minus_specular_strength = 1.0 - np.max(specular, axis=-1, keepdims=True)
        else:
            specular = specularFactor if specularFactor is not None else [1.0, 1.0, 1.0]
            specular = np.array(specular, dtype=np.float32)
            glossiness = glossinessFactor if glossinessFactor is not None else 1.0
            glossiness = np.array(glossiness, dtype=np.float32)
            one_minus_specular_strength = 1.0 - max(specular[:3])

        return specular, glossiness, one_minus_specular_strength

    if diffuseTexture is not None and specularGlossinessTexture is not None:
        # reshape to the size of the largest texture
        max_shape = [max(diffuseTexture.size[i],
                         specularGlossinessTexture.size[i]) for i in range(2)]
        if (diffuseTexture.size[0] != max_shape[0] or
                diffuseTexture.size[1] != max_shape[1]):
            diffuseTexture = diffuseTexture.resize(max_shape)
        if (specularGlossinessTexture.size[0] != max_shape[0] or
                specularGlossinessTexture.size[1] != max_shape[1]):
            specularGlossinessTexture = specularGlossinessTexture.resize(max_shape)

    diffuse = get_diffuse(diffuseFactor, diffuseTexture)
    specular, glossiness, one_minus_specular_strength = get_specular_glossiness(
        specularFactor, glossinessFactor, specularGlossinessTexture)

    metallic = solve_metallic(
        get_perceived_brightness(diffuse),
        get_perceived_brightness(specular),
        one_minus_specular_strength)
    if not isinstance(metallic, np.ndarray):
        metallic = np.array(metallic, dtype=np.float32)

    diffuse_rgb = diffuse[..., :3]
    opacity = diffuse[..., -1] if diffuse.shape[-1] == 4 else None
    base_color_from_diffuse = diffuse_rgb * (one_minus_specular_strength / (
        1.0 - dielectric_specular[0]) / np.clip((1.0 - metallic), epsilon, None))
    base_color_from_specular = (
        specular - dielectric_specular *
        (1.0 - metallic)) * (1.0 / np.clip(metallic, epsilon, None))
    mm = metallic * metallic
    base_color = mm * base_color_from_specular + (1.0 - mm) * base_color_from_diffuse
    base_color = np.clip(base_color, 0.0, 1.0)

    if opacity is not None and np.any(opacity < 1.0):
        base_color = np.concatenate([base_color, opacity[..., None]], axis=-1)

    result = {}
    if len(base_color.shape) > 1:
        result['baseColorTexture'] = toPIL(base_color)
    else:
        result['baseColorFactor'] = base_color.tolist()

    if len(metallic.shape) > 1 or len(glossiness.shape) > 1:
        if len(glossiness.shape) == 1:
            glossiness = np.tile(glossiness, (metallic.shape[0], metallic.shape[1], 1))
        if len(metallic.shape) == 1:
            metallic = np.tile(metallic, (glossiness.shape[0], glossiness.shape[1], 1))

        result['metallicRoughnessTexture'] = toPIL(
            np.concatenate([metallic, 1.0 - glossiness], axis=-1))
    else:
        result['metallicFactor'] = get_float(metallic)
        result['roughnessFactor'] = get_float(1.0 - glossiness)

    return result
