33#
44# SPDX-License-Identifier: Apache-2.0
55
6+ from __future__ import annotations
7+
68from collections .abc import Mapping
7- from typing import Any
9+ from typing import TYPE_CHECKING , Any
810
911from isaaclab .envs import ManagerBasedRLMimicEnv
1012from isaaclab .managers .recorder_manager import RecorderManagerBaseCfg
1113
1214from isaaclab_arena .assets .asset import Asset
1315from isaaclab_arena .embodiments .common .arm_mode import ArmMode
16+ from isaaclab_arena .relations .collision_mode import CollisionMode
17+ from isaaclab_arena .relations .relations import IsAnchor , Relation , RelationBase , UnaryRelation
18+ from isaaclab_arena .utils .bounding_box import AxisAlignedBoundingBox , quaternion_to_90_deg_z_quarters
1419from isaaclab_arena .utils .cameras import make_camera_observation_cfg
1520from isaaclab_arena .utils .configclass import combine_configclass_instances
21+ from isaaclab_arena .utils .embodiment_placement import PlacementUsdSource , compute_embodiment_placement_bbox
1622from isaaclab_arena .utils .pose import Pose
1723
24+ if TYPE_CHECKING :
25+ import trimesh
26+
1827
1928class EmbodimentBase (Asset ):
2029
@@ -37,6 +46,12 @@ def __init__(
3746 self .initial_pose = initial_pose
3847 self .concatenate_observation_terms = concatenate_observation_terms
3948 self .arm_mode = arm_mode or self .default_arm_mode
49+ self .relations : list [RelationBase ] = []
50+ self ._placement_bounding_box : AxisAlignedBoundingBox | None = None
51+ # None means use the solver's default collision mode for this embodiment.
52+ self .collision_mode : CollisionMode | None = None
53+ # If True, mesh collision replaces non-watertight meshes with their convex hull.
54+ self .repair_collision_mesh_non_watertight = True
4055 # These should be filled by the subclass
4156 self .scene_config : Any | None = None
4257 self .camera_config : Any | None = None
@@ -50,6 +65,70 @@ def __init__(
5065 self .xr : Any | None = None
5166 self .termination_cfg : Any | None = None
5267
68+ @property
69+ def placement_kind (self ) -> str :
70+ """Embodiments use the embodiment placement apply path."""
71+ return "embodiment"
72+
73+ @property
74+ def placement_scene_entity_name (self ) -> str :
75+ """Isaac Lab scene entity name used when writing solved poses to sim."""
76+ return self .get_embodiment_name_in_scene ()
77+
78+ def add_relation (self , relation : RelationBase ) -> None :
79+ """Attach a spatial relation to this embodiment."""
80+ assert not isinstance (relation , IsAnchor ), "Embodiment cannot be marked as an anchor"
81+ self .relations .append (relation )
82+
83+ def get_relations (self ) -> list [RelationBase ]:
84+ """Return all relations attached to this embodiment."""
85+ return self .relations
86+
87+ def get_spatial_relations (self ) -> list [RelationBase ]:
88+ """Return spatial constraints, excluding placement markers."""
89+ return [relation for relation in self .relations if isinstance (relation , (Relation , UnaryRelation ))]
90+
91+ def get_initial_pose (self ) -> Pose | None :
92+ """Return the embodiment's initial pose."""
93+ return self .initial_pose
94+
95+ @property
96+ def is_anchor (self ) -> bool :
97+ """Embodiments are never relation anchors."""
98+ return False
99+
100+ def get_placement_usd_sources (self ) -> list [PlacementUsdSource ]:
101+ """Return USD assets that define this embodiment's placement footprint."""
102+ scene_config = self .scene_config
103+ assert scene_config is not None and hasattr (
104+ scene_config , "robot"
105+ ), f"{ self .__class__ .__name__ } must populate scene_config.robot before placement bbox lookup"
106+ robot = scene_config .robot
107+ spawn = robot .spawn
108+ usd_path = spawn .usd_path
109+ scale = getattr (spawn , "scale" , None ) or (1.0 , 1.0 , 1.0 )
110+ if not isinstance (scale , tuple ):
111+ scale = tuple (scale )
112+ return [PlacementUsdSource (usd_path = usd_path , scale = scale )]
113+
114+ def get_bounding_box (self ) -> AxisAlignedBoundingBox :
115+ """Return the USD-derived local placement footprint relative to the robot origin."""
116+ if self ._placement_bounding_box is None :
117+ self ._placement_bounding_box = compute_embodiment_placement_bbox (self .get_placement_usd_sources ())
118+ return self ._placement_bounding_box
119+
120+ def get_world_bounding_box (self ) -> AxisAlignedBoundingBox :
121+ """Return the placement footprint in world coordinates."""
122+ local_bbox = self .get_bounding_box ()
123+ pose = self .get_initial_pose ()
124+ if pose is None :
125+ return local_bbox
126+ quarters = quaternion_to_90_deg_z_quarters (pose .rotation_xyzw )
127+ return local_bbox .rotated_90_around_z (quarters ).translated (pose .position_xyz )
128+
129+ def get_collision_mesh (self ) -> trimesh .Trimesh | None :
130+ """Embodiments fall back to AABB overlap checks in the relation solver."""
131+
53132 def set_initial_pose (self , pose : Pose ) -> None :
54133 self .initial_pose = pose
55134
0 commit comments