Source code for flow360_schema.models.simulation.camera

"""Shared camera schema for renders and reports.

Defines a single :class:`Camera` — a viewpoint camera (eye *direction* plus framing,
in model units) used directly by both ``RenderOutput.camera`` and the report camera,
and the :class:`Viewpoint` enum of canonical view directions.

The absolute camera is resolved downstream from the scene bounding box: ``position``
is a direction on the unit sphere about ``look_at`` (world position =
``look_at + 4 * bounding_sphere_radius * normalize(position)``); ``look_at`` defaults
to the scene bounding-box center; ``dimension`` (measured along ``dimension_dir``)
sets the orthographic frustum extent, or fits the scene when ``None``.

Perspective projection is a future addition; the viewpoint currently resolves to an
orthographic camera.
"""

from enum import Enum
from typing import Literal

import pydantic as pd

from flow360_schema.framework.base_model import Flow360BaseModel


[docs] class Camera(Flow360BaseModel): """ A viewpoint camera shared by :class:`RenderOutput` and the report — an eye *direction* plus framing, expressed in plain floats measured in model (geometry / mesh) units. Example ------- >>> Camera(position=(-1, -1, 1), dimension=2.5) >>> Camera(position=Viewpoint.TOP + Viewpoint.LEFT) # frames the scene """ position: tuple[float, float, float] | None = pd.Field( (-1, -1, 1), description="Eye position as a DIRECTION from look_at on the unit sphere (not an absolute " "position); the absolute position is resolved against the scene bounding box downstream. " "Model units.", ) up: tuple[float, float, float] | None = pd.Field((0, 0, 1), description="Up vector, if not specified assume Z+") look_at: tuple[float, float, float] | None = pd.Field( None, description="Look-at target; if None, defaults to the scene bounding-box center. Model units.", ) pan_target: tuple[float, float, float] | None = pd.Field( None, description="Viewport pan-center; if None, defaults to look_at. On a render this recenters the " "frustum (off-center) so pan_target lands at the viewport center. Model units.", ) dimension_dir: Literal["width", "height", "diagonal"] | None = pd.Field( "diagonal", alias="dimensionDirection", description="Which viewport extent 'dimension' measures. Defaults to 'diagonal' to match the " "webUI viewer. 'height'/'diagonal' are resolved to frustum width using the render output " "aspect ratio; 'width' needs no aspect", ) dimension: float | None = pd.Field( None, alias="dimensionSizeModelUnits", description="Framing size in model units measured along dimension_dir; sets the orthographic " "frustum extent. If None, the framing fits the scene bounding box.", ) type: Literal["Camera"] = pd.Field("Camera", frozen=True) @pd.model_validator(mode="before") @classmethod def _accept_type_name_alias(cls, data): """Accept ``type_name`` as an input alias for the ``type`` discriminator. RenderOutput/simulation.json (and the schema-wide convention, incl. LegacyCamera) key this discriminator as ``type_name``, while the report/UVF-JSON camera hierarchy uses ``type``. A field-level alias can't be used because ``type`` is the discriminator of the report ``Chart3D.camera`` union and pydantic forbids aliased discriminators, so normalize here instead. Serialization stays ``type`` (report JSON unchanged). """ if isinstance(data, dict) and "type_name" in data: data = {**data} data.setdefault("type", data.pop("type_name")) return data
class Viewpoint(Enum): """ :class:`Viewpoint` provides predefined canonical view directions, usable as a :class:`Camera` ``position``. Example ------- >>> Viewpoint.FRONT.value (-1, 0, 0) >>> Viewpoint.FRONT + Viewpoint.TOP (-1, 0, 1) """ FRONT = (-1, 0, 0) BACK = (1, 0, 0) RIGHT = (0, -1, 0) LEFT = (0, 1, 0) TOP = (0, 0, 1) BOTTOM = (0, 0, -1) def __getitem__(self, idx): return self.value[idx] def __add__(self, other): if isinstance(other, Viewpoint): b = other.value elif isinstance(other, tuple): b = other else: return NotImplemented a = self.value return tuple(x + y for x, y in zip(a, b, strict=False)) def __radd__(self, other): if isinstance(other, tuple): a = other elif isinstance(other, Viewpoint): a = other.value else: return NotImplemented b = self.value return tuple(x + y for x, y in zip(a, b, strict=False))