Source code for scenic.core.shapes

""" Module containing the Shape class and its subclasses, which represent shapes of Objects"""

from abc import ABC, abstractmethod

import numpy
import trimesh
from trimesh.transformations import (
    concatenate_matrices,
    quaternion_matrix,
    translation_matrix,
)

from scenic.core.type_support import toOrientation
from scenic.core.utils import cached_property, unifyMesh
from scenic.core.vectors import Orientation

###################################################################################################
# Abstract Classes and Utilities
###################################################################################################


[docs]class Shape(ABC): """An abstract base class for Scenic shapes. Represents a physical shape in Scenic. Does not encode position or orientation, which are handled by the `Region` class. Does contain dimension information, which is used as a default value by any `Object` with this shape and can be overwritten. If dimensions and scale are both specified the dimensions are first set by dimensions, and then scaled by scale. Args: dimensions: The raw (before scaling) dimensions of the shape. scale: Scales all the dimensions of the shape by a multiplicative factor. """ def __init__(self, dimensions, scale): # Store values self.raw_dimensions = dimensions self.scale = scale self.dimensions = tuple(dim * self.scale for dim in self.raw_dimensions) self.width = self.dimensions[0] self.length = self.dimensions[1] self.height = self.dimensions[2] for dim, name in zip(self.dimensions, ("width", "length", "height")): if dim <= 0: raise ValueError(f"{name} of shape must be positive") @cached_property def containsCenter(self): """Whether or not this object contains its central point""" pq = trimesh.proximity.ProximityQuery(self.mesh) region_distance = pq.signed_distance([(0, 0, 0)])[0] return region_distance > 0 @property @abstractmethod def mesh(self): pass @property @abstractmethod def isConvex(self): pass
################################################################################################### # 3D Shape Classes ###################################################################################################
[docs]class MeshShape(Shape): """A Shape subclass defined by a `trimesh.base.Trimesh` object. The mesh passed must be a `trimesh.base.Trimesh` object that represents a well defined volume (i.e. the ``is_volume`` property must be true), meaning the mesh must be watertight, have consistent winding and have outward facing normals. Args: mesh: A mesh object. dimensions: The raw (before scaling) dimensions of the shape. If dimensions and scale are both specified the dimensions are first set by dimensions, and then scaled by scale. scale: Scales all the dimensions of the shape by a multiplicative factor. If dimensions and scale are both specified the dimensions are first set by dimensions, and then scaled by scale. initial_rotation: A 3-tuple containing the yaw, pitch, and roll respectively to apply when loading the mesh. Note the initial_rotation must be fixed. """ def __init__(self, mesh, dimensions=None, scale=1, initial_rotation=None): # Ensure the mesh is watertight so volume is well defined if not mesh.is_volume: raise ValueError( "A MeshShape cannot be defined with a mesh that does not have a well defined volume." " Consider using scenic.core.utils.repairMesh." ) # Copy mesh and center vertices around origin self._mesh = mesh.copy() self._mesh.vertices -= self._mesh.bounding_box.center_mass # If rotation is provided, apply rotation if initial_rotation is not None: rotation = toOrientation(initial_rotation) rotation_matrix = quaternion_matrix( (rotation.w, rotation.x, rotation.y, rotation.z) ) self._mesh.apply_transform(rotation_matrix) # If dimensions are not specified, infer them. if dimensions is None: dimensions = list(self._mesh.extents) # Scale mesh to unit size scale_vals = numpy.array(self._mesh.extents) scale_matrix = numpy.eye(4) scale_matrix[:3, :3] /= scale_vals self._mesh.apply_transform(scale_matrix) super().__init__(dimensions, scale)
[docs] @classmethod def fromFile(cls, path, unify=True, **kwargs): """Load a mesh shape from a file, attempting to infer filetype and compression. For example: "foo.obj.bz2" is assumed to be a compressed .obj file. "foo.obj" is assumed to be an uncompressed .obj file. "foo" is an unknown filetype, so unless a filetype is provided an exception will be raised. Args: path (str): Path to the file to import. filetype (str): Filetype of file to be imported. This will be inferred if not provided. The filetype must be one compatible with `trimesh.load`. compressed (bool): Whether or not this file is compressed (with bz2). This will be inferred if not provided. binary (bool): Whether or not to open the file as a binary file. unify (bool): Whether or not to attempt to unify this mesh. kwargs: Additional arguments to the MeshShape initializer. """ mesh = trimesh.load(path, force="mesh") if not mesh.is_volume: raise ValueError( "A MeshShape cannot be defined with a mesh that does not have a well defined volume." " Consider using scenic.core.utils.repairMesh." ) if unify: mesh = unifyMesh(mesh, verbose=True) return cls(mesh, **kwargs)
@property def mesh(self): return self._mesh @cached_property def isConvex(self): return self._mesh.is_convex def __getstate__(self): state = self.__dict__.copy() state["_mesh"] = self._mesh.copy() return state
[docs]class BoxShape(MeshShape): """A box shape with all dimensions 1 by default.""" def __init__(self, dimensions=(1, 1, 1), scale=1, initial_rotation=None): super().__init__( trimesh.creation.box((1, 1, 1)), dimensions, scale, initial_rotation )
[docs]class CylinderShape(MeshShape): """A cylinder shape with all dimensions 1 by default.""" def __init__(self, dimensions=(1, 1, 1), scale=1, initial_rotation=None, sections=24): super().__init__( trimesh.creation.cylinder(radius=0.5, height=1, sections=sections), dimensions, scale, initial_rotation, ) self.sections = sections
[docs]class ConeShape(MeshShape): """A cone shape with all dimensions 1 by default.""" def __init__(self, dimensions=(1, 1, 1), scale=1, initial_rotation=None): super().__init__( trimesh.creation.cone(radius=0.5, height=1), dimensions, scale, initial_rotation, )
[docs]class SpheroidShape(MeshShape): """A spheroid shape with all dimensions 1 by default.""" def __init__(self, dimensions=(1, 1, 1), scale=1, initial_rotation=None): super().__init__( trimesh.creation.icosphere(radius=1), dimensions, scale, initial_rotation )