# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------
"""Base asset configuration and file-backed physics policy."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Dict, Literal
import numpy as np
from embodichain.utils import configclass, is_configclass, logger
AssetPhysicsMode = Literal["preserve", "overlay"]
"""Policy for applying EmbodiChain physics to a file-backed asset."""
def _resolve_asset_physics_mode(
mode: AssetPhysicsMode,
) -> AssetPhysicsMode:
"""Validate and return a source-agnostic asset-physics policy."""
if mode not in ("preserve", "overlay"):
raise ValueError(
f"asset_physics_mode must be 'preserve' or 'overlay', got {mode!r}."
)
return mode
[docs]
@configclass
class ObjectBaseCfg:
"""Base configuration for an asset in the simulation.
This class defines the basic properties of an asset, such as its type, initial state, and collision group.
It is used as a base class for specific asset configurations.
"""
uid: str | None = None
init_pos: tuple[float, float, float] = (0.0, 0.0, 0.0)
"""Position of the root in simulation world frame. Defaults to (0.0, 0.0, 0.0)."""
init_rot: tuple[float, float, float] = (0.0, 0.0, 0.0)
"""Euler angles (in degree) of the root in simulation world frame. Defaults to (0.0, 0.0, 0.0)."""
init_local_pose: np.ndarray | None = None
"""4x4 transformation matrix of the root in local frame. If specified, it will override init_pos and init_rot."""
[docs]
@classmethod
def from_dict(cls, init_dict: Dict[str, str | float | tuple]) -> ObjectBaseCfg:
"""Initialize the configuration from a dictionary."""
from .source import _raise_removed_source_fields, _usd_source_from_value
_raise_removed_source_fields(init_dict)
cfg = cls() # Create a new instance of the class (cls)
for key, value in init_dict.items():
if hasattr(cfg, key):
attr = getattr(cfg, key)
if key == "source":
setattr(cfg, key, _usd_source_from_value(value))
elif key == "shape" and isinstance(value, Mapping):
from ..shapes import ShapeCfg
setattr(cfg, key, ShapeCfg.from_dict(dict(value)))
elif key == "attrs" and isinstance(value, Mapping):
# Keep the base module independent of rigid schemas at
# import time; only rigid-derived configs expose this key.
from .rigid import _rigid_body_physics_from_dict
setattr(cfg, key, _rigid_body_physics_from_dict(value))
elif is_configclass(attr):
setattr(
cfg, key, attr.from_dict(value)
) # Call from_dict on the attribute
else:
setattr(cfg, key, value)
else:
logger.log_warning(
f"Key '{key}' not found in {cfg.__class__.__name__}."
)
# Automatically infer init_local_pose if not provided
if cfg.init_local_pose is None:
# If only init_pos or init_rot are provided, generate the 4x4 pose matrix
from scipy.spatial.transform import Rotation as R
T = np.eye(4)
T[:3, 3] = np.array(cfg.init_pos)
T[:3, :3] = R.from_euler("xyz", np.deg2rad(cfg.init_rot)).as_matrix()
cfg.init_local_pose = T
else:
# If only init_local_pose is provided, extract init_pos and init_rot
from scipy.spatial.transform import Rotation as R
T = np.array(cfg.init_local_pose)
cfg.init_pos = tuple(T[:3, 3])
cfg.init_rot = tuple(R.from_matrix(T[:3, :3]).as_euler("xyz", degrees=True))
return cfg