Source code for embodichain.lab.sim.scene.placement

# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------

"""Pure placement proposals for explicit scene support geometry."""

from __future__ import annotations

import numpy as np

from embodichain.lab.sim.scene._packing_cpu import AABBLayoutConfig, AABBLayoutOptimizer
from embodichain.lab.sim.scene._placement_cpu import (
    GroupSupportClamp,
    GroupSupportClampConfig,
    group_height_offset,
)
from .geometry import SupportRegion

__all__ = [
    "AABBLayoutConfig",
    "GroupSupportClampConfig",
    "clamp_group",
    "separate_aabbs",
    "align_group_height",
]


[docs] def clamp_group( region: SupportRegion, aabbs_by_uid: dict[str, np.ndarray], *, config: GroupSupportClampConfig | None = None, ) -> np.ndarray | None: """Propose one planar group translation retaining all relative positions. Args: region: Supporting entity and allowed arena-local XY region. aabbs_by_uid: Four XY corners per entity in arena coordinates. config: Boundary clearance and raster-search resolution. Returns: Shared XY offset, or ``None`` if the raster search found no placement. Failure does not establish mathematical infeasibility. """ return GroupSupportClamp(config=config).find_translation( region.support_polygon, aabbs_by_uid )
[docs] def separate_aabbs( region: SupportRegion, aabbs_by_uid: dict[str, np.ndarray], *, config: AABBLayoutConfig | None = None, ) -> dict[str, np.ndarray]: """Propose independent XY translations resolving projected AABB overlaps. Args: region: Supporting entity and allowed arena-local XY region. aabbs_by_uid: Four XY corners per entity in arena coordinates. config: Boundary margin, pair clearance and iteration budget. Returns: Translation per logical entity UID; no simulation is modified. Raises: ValueError: Invalid geometry or no solution found within the budget. .. attention:: This checks planar AABBs, not exact mesh collisions or physical stability. """ return AABBLayoutOptimizer(config=config).solve( region.support_polygon, aabbs_by_uid )
[docs] def align_group_height( region: SupportRegion, bounds_by_uid: dict[str, np.ndarray], *, clearance_m: float = 0.02, ) -> float: """Propose one Z translation aligning the group's lowest point to support. Args: region: Horizontal support descriptor in arena coordinates. bounds_by_uid: Per-entity minima/maxima with shape ``(2, 3)``. clearance_m: Non-negative gap above support, in metres. Returns: Shared vertical translation. Positions and orientations are unchanged. """ if not bounds_by_uid or any( not isinstance(uid, str) or not uid for uid in bounds_by_uid ): raise ValueError("At least one non-empty entity UID and bounds are required.") return group_height_offset( region.top_z, np.asarray(list(bounds_by_uid.values())), clearance_m=clearance_m )