Source code for embodichain.lab.sim.scene.geometry
# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------
"""Explicit Z-up support geometry for scene placement services."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
import numpy as np
from shapely.geometry import MultiPolygon, Polygon
from embodichain.lab.sim.scene._support_cpu import (
SupportSurfaceConfig,
SupportSurfaceDetector,
)
__all__ = ["SupportSurfaceConfig", "SupportRegion", "detect_support_region"]
[docs]
@dataclass(frozen=True)
class SupportRegion:
"""A horizontal support identified by an existing scene entity UID.
Geometry is measured in metres in arena-local Z-up coordinates. Polygon
holes and disconnected regions are supported by placement; mesh detection
selects one largest contour, retaining openings when ``preserve_holes`` is
enabled in :class:`SupportSurfaceConfig`.
This descriptor neither owns nor creates simulation resources.
"""
support_uid: str
top_z: float
support_polygon: Polygon | MultiPolygon
optimization_rectangle: Polygon | None = None
frame: Literal["arena"] = "arena"
def __post_init__(self) -> None:
if not isinstance(self.support_uid, str) or not self.support_uid:
raise ValueError("support_uid must be a non-empty entity UID.")
if self.frame != "arena":
raise ValueError("Support regions require the arena-local Z-up frame.")
if not np.isfinite(self.top_z):
raise ValueError("Support height must be finite.")
polygon = self.support_polygon
if (
not isinstance(polygon, (Polygon, MultiPolygon))
or polygon.is_empty
or not polygon.is_valid
or polygon.area <= 0
or not np.isfinite(polygon.bounds).all()
):
raise ValueError(
"Support must be a finite, valid, non-empty polygonal region."
)
rectangle = self.optimization_rectangle
if rectangle is not None and (
not isinstance(rectangle, Polygon)
or rectangle.is_empty
or not rectangle.is_valid
or not polygon.covers(rectangle)
):
raise ValueError("Optimization rectangle must be contained in support.")
[docs]
def detect_support_region(
support_uid: str,
vertices: np.ndarray,
faces: np.ndarray,
*,
config: SupportSurfaceConfig | None = None,
) -> SupportRegion:
"""Detect one main horizontal support from explicit arena-local geometry.
Args:
support_uid: UID of the existing supporting entity.
vertices: Already transformed Z-up vertices in metres, shape ``(N, 3)``.
faces: Triangle indices, shape ``(M, 3)``.
config: Geometric support selection thresholds.
Returns:
Support descriptor in the same frame, without applying asset scale again.
"""
detected = SupportSurfaceDetector(vertices, faces, config=config).detect()
return SupportRegion(
support_uid=support_uid,
top_z=detected.top_z,
support_polygon=detected.support_polygon,
optimization_rectangle=detected.optimization_rectangle,
)