Source code for embodichain.lab.sim.objects.backends.collision

# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
"""Backend compatibility helpers for rigid-object collision geometry."""

from __future__ import annotations

from typing import TYPE_CHECKING

import torch
from dexsim.engine import (
    BoxGeometry,
    CapsuleGeometry,
    ConvexMeshGeometry,
    PlaneGeometry,
    SDFGeometry,
    SphereGeometry,
    TriangleMeshGeometry,
)
from dexsim.spawn.descs import GeometryType
from dexsim.types import RigidBodyShape

if TYPE_CHECKING:
    from ..rigid_object import CollisionShapeDesc

__all__ = ["collision_shapes_from_entity"]


[docs] def collision_shapes_from_entity( entity: object, *, object_uid: str, ) -> list[CollisionShapeDesc]: """Read one entity's physical collision shapes across DexSim backends. DexSim versions before the SpawnedRigidBody facade exposed a native ``get_physical_body`` method. Newer facades retain collision descriptors and expose Newton's collision mesh through the private binding instead. This adapter keeps those backend-specific details out of ``RigidObject``. Args: entity: Materialized DexSim rigid-body handle. object_uid: Logical EmbodiChain object ID used in diagnostics. Returns: Planner-independent collision-shape snapshots. Raises: RuntimeError: If no supported physical collision representation exists. """ from ..rigid_object import CollisionShapeDesc get_physical_body = getattr(entity, "get_physical_body", None) physical_body = get_physical_body() if callable(get_physical_body) else None if physical_body is None: native = entity.native() if callable(getattr(entity, "native", None)) else None native_get_physical_body = getattr(native, "get_physical_body", None) physical_body = ( native_get_physical_body() if callable(native_get_physical_body) else None ) if physical_body is not None: return _from_physical_body(physical_body, object_uid=object_uid) descriptor_shapes = _from_descriptor(entity) if descriptor_shapes is not None: return descriptor_shapes # Newton's runtime binding exposes a complete local collision mesh even # when it does not expose the legacy physical-body shape API. This is a # fallback for mesh/convex/SDF shapes and future geometry kinds. binding = getattr(entity, "_physics_binding", None) get_collision_mesh = getattr(binding, "get_collision_mesh", None) if callable(get_collision_mesh): collision_mesh = get_collision_mesh() if collision_mesh is not None: vertices, triangles = collision_mesh return [ CollisionShapeDesc( name="collision_mesh", shape_type=RigidBodyShape.MESH, local_pose=torch.eye(4, dtype=torch.float32), vertices=torch.as_tensor(vertices, dtype=torch.float32) .reshape(-1, 3) .clone(), triangles=torch.as_tensor(triangles, dtype=torch.int32) .reshape(-1, 3) .clone(), ) ] raise RuntimeError( f"RigidObject {object_uid!r} has no accessible physical collision " "descriptor." )
def _from_physical_body( physical_body: object, *, object_uid: str, ) -> list[CollisionShapeDesc]: """Extract collision descriptors from the legacy native body API.""" from ..rigid_object import CollisionShapeDesc shape_count = int(physical_body.get_shape_count()) shapes: list[CollisionShapeDesc] = [] for shape_idx in range(shape_count): try: geometry = physical_body.get_shape_geometry(shape_idx) except Exception as exc: # noqa: BLE001 raise RuntimeError( f"DexSim could not expose collision shape {shape_idx} for " f"RigidObject {object_uid!r}. SDF/custom shapes require a " "geometry descriptor or canonical collision mesh." ) from exc if geometry is None: raise RuntimeError( f"DexSim returned no geometry for collision shape {shape_idx} " f"of RigidObject {object_uid!r}." ) shape_name = physical_body.get_shape_name(shape_idx) or f"shape_{shape_idx}" local_pose = torch.tensor(geometry.local_pose, dtype=torch.float32) if local_pose.shape != (4, 4): raise RuntimeError( f"DexSim collision shape {shape_idx} of {object_uid!r} returned " f"local_pose shape {tuple(local_pose.shape)}, expected (4, 4)." ) desc = CollisionShapeDesc( name=str(shape_name), shape_type=_collision_shape_type(geometry), local_pose=local_pose.clone(), ) if isinstance(geometry, BoxGeometry): desc.half_extents = torch.tensor(geometry.half_extents, dtype=torch.float32) elif isinstance(geometry, SphereGeometry): desc.radius = float(geometry.radius) elif isinstance(geometry, CapsuleGeometry): desc.radius = float(geometry.radius) desc.half_height = float(geometry.half_height) elif isinstance( geometry, (ConvexMeshGeometry, TriangleMeshGeometry, SDFGeometry) ): vertices = torch.tensor(geometry.vertices, dtype=torch.float32).reshape( -1, 3 ) triangles = torch.tensor(geometry.triangles, dtype=torch.int32).reshape( -1, 3 ) scale = getattr(geometry, "scale", None) if scale is not None: vertices = vertices * torch.tensor(scale, dtype=torch.float32).reshape( 1, 3 ) desc.vertices = vertices desc.triangles = triangles shapes.append(desc) return shapes def _from_descriptor(entity: object) -> list[CollisionShapeDesc] | None: """Translate retained Spawn descriptors when native shape access is absent.""" from ..rigid_object import CollisionShapeDesc object_desc = getattr(entity, "object_desc", None) collisions = getattr(object_desc, "collisions", None) if not collisions: return None body_scale = torch.as_tensor( getattr(object_desc, "body_scale", (1.0, 1.0, 1.0)), dtype=torch.float32, ).reshape(3) shapes: list[CollisionShapeDesc] = [] for shape_idx, collision in enumerate(collisions): if getattr(collision, "enable_collision", True) is False: continue geometry_type = getattr(collision, "geometry_type", None) geometry_type = getattr(geometry_type, "value", geometry_type) local_pose = torch.as_tensor( getattr(collision, "local_transform", torch.eye(4)), dtype=torch.float32, ).reshape(4, 4) name = str(getattr(collision, "segment_name", None) or f"shape_{shape_idx}") if geometry_type == GeometryType.CUBE.value: size = torch.as_tensor(collision.size, dtype=torch.float32).reshape(3) shapes.append( CollisionShapeDesc( name=name, shape_type=RigidBodyShape.BOX, local_pose=local_pose.clone(), half_extents=(0.5 * size * body_scale).clone(), ) ) continue if geometry_type == GeometryType.SPHERE.value: shapes.append( CollisionShapeDesc( name=name, shape_type=RigidBodyShape.SPHERE, local_pose=local_pose.clone(), radius=float(collision.radius) * float(body_scale[0]), ) ) continue if geometry_type == GeometryType.CAPSULE.value: scale = float(body_scale[0]) shapes.append( CollisionShapeDesc( name=name, shape_type=RigidBodyShape.CAPSULE, local_pose=local_pose.clone(), radius=float(collision.radius) * scale, half_height=float(collision.half_height) * scale, ) ) continue if geometry_type == GeometryType.PLANE.value: shapes.append( CollisionShapeDesc( name=name, shape_type=RigidBodyShape.PLANE, local_pose=local_pose.clone(), ) ) continue vertices = getattr(collision, "vertices", None) triangles = getattr(collision, "triangles", None) if vertices is not None and triangles is not None: shapes.append( CollisionShapeDesc( name=name, shape_type=RigidBodyShape.MESH, local_pose=local_pose.clone(), vertices=( torch.as_tensor(vertices, dtype=torch.float32) .reshape(-1, 3) .mul(body_scale.reshape(1, 3)) .clone() ), triangles=torch.as_tensor(triangles, dtype=torch.int32) .reshape(-1, 3) .clone(), ) ) continue return None return shapes def _collision_shape_type(geometry: object) -> RigidBodyShape: """Map a concrete DexSim geometry descriptor to its shape enum.""" if isinstance(geometry, BoxGeometry): return RigidBodyShape.BOX if isinstance(geometry, PlaneGeometry): return RigidBodyShape.PLANE if isinstance(geometry, SphereGeometry): return RigidBodyShape.SPHERE if isinstance(geometry, CapsuleGeometry): return RigidBodyShape.CAPSULE if isinstance(geometry, ConvexMeshGeometry): return RigidBodyShape.CONVEX if isinstance(geometry, TriangleMeshGeometry): return RigidBodyShape.MESH if isinstance(geometry, SDFGeometry): return RigidBodyShape.SDF raise RuntimeError( f"Unsupported DexSim collision geometry descriptor " f"{type(geometry).__name__}." )