"""Mirror plane, mirrored entities and helpers."""
from __future__ import annotations
from flow360_schema.framework.entity.entity_registry import (
EntityRegistry,
EntityRegistryView,
)
from flow360_schema.framework.entity.mirror_state import (
MIRROR_SUFFIX,
MirrorState,
_derive_mirrored_entities_from_actions,
)
from flow360_schema.models.asset_cache import MirrorStatus
from flow360_schema.models.entities.geometry_entities import (
GeometryBodyGroup,
MirrorPlane,
)
from flow360_schema.models.entities.surface_entities import (
MirroredGeometryBodyGroup,
MirroredSurface,
)
from flow360_schema.models.simulation.utils import is_exact_instance
from flow360.exceptions import Flow360ValueError
# pylint: disable=protected-access
__all__ = [
"MIRROR_SUFFIX",
"MirrorManager",
"MirrorPlane",
"MirrorStatus",
"_derive_mirrored_entities_from_actions",
]
[docs]
class MirrorManager:
"""
Manage mirror planes and mirrored draft entities inside a `DraftContext`.
This manager provides:
- Storage/registration of `MirrorPlane` entities.
- Creation/removal of mirror actions for `GeometryBodyGroup` entities.
- Derivation and registration of draft-only entities:
`MirroredGeometryBodyGroup` and (when possible) `MirroredSurface`.
Notes
-----
Surface mirroring requires a surface-to-body-group mapping derived from `GeometryEntityInfo`.
If that mapping cannot be derived (e.g. face grouping spans multiple body groups), mirroring
is disabled and mirror operations will raise.
"""
# Keep the manager as a thin client-facing facade. We intentionally use composition
# instead of inheriting from MirrorState so the client does not implicitly expose
# every schema-core helper as part of its API surface.
__slots__ = ("_state",)
def __init__(
self,
*,
face_group_to_body_group: dict[str, str] | None = None,
entity_registry: EntityRegistry | None = None,
state: MirrorState | None = None,
) -> None:
if state is None:
if entity_registry is None:
raise Flow360ValueError(
"[Internal] MirrorManager requires `entity_registry` when `state` is not provided."
)
state = MirrorState(
face_group_to_body_group=face_group_to_body_group,
entity_registry=entity_registry,
)
self._state = state
@property
def mirror_planes(self) -> EntityRegistryView:
"""
Return all the available mirror planes.
Returns
-------
EntityRegistryView
A registry view of `MirrorPlane` entities available in this draft.
"""
return self._state.mirror_planes
@property
def _mirror_status(self) -> MirrorStatus:
"""Read-only compatibility surface for existing draft plumbing and tests."""
return self._state._mirror_status
@property
def _body_group_id_to_mirror_id(self) -> dict[str, str]:
"""Read-only compatibility surface for existing draft plumbing and tests."""
return self._state._body_group_id_to_mirror_id
@property
def _mirror_planes(self) -> list[MirrorPlane]:
"""Read-only compatibility surface for existing draft plumbing and tests."""
return self._state._mirror_planes
[docs]
def create_mirror_of(
self,
*,
entities: list[GeometryBodyGroup] | GeometryBodyGroup,
mirror_plane: MirrorPlane,
) -> tuple[list[MirroredGeometryBodyGroup], list[MirroredSurface]]:
"""
Create mirrored entities for one or more geometry body groups.
This registers mirror actions for the requested body groups and then derives/creates
draft-only entities:
- `MirroredGeometryBodyGroup` for each body group
- `MirroredSurface` for each surface belonging to those body groups
(when surface ownership mapping is available)
Newly created mirrored entities use `MIRROR_SUFFIX` (``"_<mirror>"``) as a name suffix.
Parameters
----------
entities : list[GeometryBodyGroup] | GeometryBodyGroup
One or more geometry body groups to mirror.
mirror_plane : MirrorPlane
The mirror plane to use for mirroring.
Returns
-------
tuple[list[MirroredGeometryBodyGroup], list[MirroredSurface]]
Mirrored geometry body groups and surfaces.
"""
if isinstance(entities, GeometryBodyGroup):
normalized_entities = [entities]
elif isinstance(entities, list):
normalized_entities = entities
else:
raise Flow360ValueError(
f"`entities` accepts a single entity or a list of entities. Received type: {type(entities).__name__}."
)
for entity in normalized_entities:
if not is_exact_instance(entity, GeometryBodyGroup):
raise Flow360ValueError(
"Only GeometryBodyGroup entities are supported by `create()` currently. "
f"Received: {type(entity).__name__}."
)
return self._state.create_mirror_of(
entities=normalized_entities,
mirror_plane=mirror_plane,
)
[docs]
def remove_mirror_of(self, *, entities: list[GeometryBodyGroup] | GeometryBodyGroup) -> None:
"""
Remove the mirror of the given entities.
Parameters
----------
entities : list[GeometryBodyGroup] | GeometryBodyGroup
One or more geometry body groups to remove mirroring from.
"""
self._state.remove_mirror_of(entities=entities)
@classmethod
def _from_status(
cls,
*,
status: MirrorStatus | None,
face_group_to_body_group: dict[str, str] | None,
entity_registry: EntityRegistry,
) -> "MirrorManager":
"""
Restore manager from a status snapshot.
Parameters
----------
status : MirrorStatus | None
The mirror status to restore from.
face_group_to_body_group : dict[str, str] | None
Mapping from surface name to owning body group ID.
entity_registry : EntityRegistry
Entity registry containing body groups and surfaces.
Returns
-------
MirrorManager
Restored mirror manager.
"""
if status is None or status.is_empty():
return cls(
face_group_to_body_group=face_group_to_body_group,
entity_registry=entity_registry,
)
return cls(
state=MirrorState._from_status(
status=status,
face_group_to_body_group=face_group_to_body_group,
entity_registry=entity_registry,
)
)