"""
parent.py
-------------
The base class for Trimesh, PointCloud, and Scene objects
"""
import abc
import numpy as np
from . import bounds, caching
from . import transformations as tf
from .caching import cache_decorator
from .constants import tol
from .typed import Any, ArrayLike, Dict, NDArray, Optional
from .util import ABC
[docs]
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.
    """
    # geometry should have a dict to store loose metadata
    metadata: Dict
    @property
    @abc.abstractmethod
    def bounds(self) -> NDArray[np.float64]:
        pass
    @property
    @abc.abstractmethod
    def extents(self) -> NDArray[np.float64]:
        pass
    @property
    @abc.abstractmethod
    def is_empty(self) -> bool:
        pass
[docs]
    def __hash__(self):
        """
        Get a hash of the current geometry.
        Returns
        ---------
        hash : int
          Hash of current graph and geometry.
        """
        return self._data.__hash__()  # type: ignore 
[docs]
    @abc.abstractmethod
    def copy(self):
        pass 
[docs]
    @abc.abstractmethod
    def show(self):
        pass 
    @abc.abstractmethod
    def __add__(self, other):
        pass
[docs]
    @abc.abstractmethod
    def export(self, file_obj, file_type=None):
        pass 
[docs]
    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(f"vertices.shape={self.vertices.shape}")
        if hasattr(self, "faces"):
            # for Trimesh
            elements.append(f"faces.shape={self.faces.shape}")
        if hasattr(self, "geometry") and isinstance(self.geometry, dict):
            # for Scene
            elements.append(f"len(geometry)={len(self.geometry)}")
        if "Voxel" in type(self).__name__:
            # for VoxelGrid objects
            elements.append(str(self.shape)[1:-1])
        if "file_name" in self.metadata:
            display = self.metadata["file_name"]
            elements.append(f"name=`{display}`")
        return "<trimesh.{}({})>".format(type(self).__name__, ", ".join(elements)) 
[docs]
    def apply_translation(self, translation: ArrayLike):
        """
        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) 
[docs]
    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) 
[docs]
    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)) 
    @cache_decorator
    def scale(self) -> float:
        """
        A loosely specified "order of magnitude scale" for the
        geometry which always returns a value and can be used
        to make code more robust to large scaling differences.
        It returns the diagonal of the axis aligned bounding box
        or if anything is invalid or undefined, `1.0`.
        Returns
        ----------
        scale : float
          Approximate order of magnitude scale of the geometry.
        """
        # if geometry is empty return 1.0
        if self.extents is None:
            return 1.0
        # get the length of the AABB diagonal
        scale = float((self.extents**2).sum() ** 0.5)
        if scale < tol.zero:
            return 1.0
        return scale
    @property
    def units(self) -> Optional[str]:
        """
        Definition of units for the mesh.
        Returns
        ----------
        units : str
          Unit system mesh is in, or None if not defined
        """
        return self.metadata.get("units", None)
    @units.setter
    def units(self, value: str) -> None:
        """
        Define the units of the current mesh.
        """
        self.metadata["units"] = str(value).lower().strip() 
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)
        return primitives.Box(transform=transform, extents=self.extents, mutable=False)
    @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 bounds, primitives
        to_origin, extents = bounds.oriented_bounds(self)
        return primitives.Box(
            transform=np.linalg.inv(to_origin), extents=extents, mutable=False
        )
    @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 nsphere, primitives
        center, radius = nsphere.minimum_nsphere(self)
        return primitives.Sphere(center=center, radius=radius, mutable=False)
    @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 bounds, primitives
        kwargs = bounds.minimum_cylinder(self)
        return primitives.Cylinder(mutable=False, **kwargs)
    @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])
        return options[volume_min]
    def apply_obb(self, **kwargs):
        """
        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.
        Parameters
        ------------
        kwargs
          Passed through to `bounds.oriented_bounds`
        Returns
        ----------
        matrix : (4, 4) float
          Transformation matrix that was applied
          to mesh to move it into OBB frame
        """
        # save the pre-transform volume
        if tol.strict and hasattr(self, "volume"):
            volume = self.volume
        # calculate the OBB passing keyword arguments through
        matrix, extents = bounds.oriented_bounds(self, **kwargs)
        # apply the transform
        self.apply_transform(matrix)
        if tol.strict:
            # obb transform should not have changed volume
            if hasattr(self, "volume") and getattr(self, "is_watertight", False):
                assert np.isclose(self.volume, volume)
            # overall extents should match what we expected
            assert np.allclose(self.extents, extents)
        return matrix