Source code for optiland.nonsequential.visualization.viewer_3d
"""NSQViewer3D -- 3D VTK visualization for NSQ scenes.
Kramer Harrison, 2026
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from optiland.visualization.base import BaseViewer3D
if TYPE_CHECKING:
from optiland.nonsequential.scene import NSQScene
from optiland.nonsequential.tracer import SimulationResult
[docs]
class NSQViewer3D(BaseViewer3D):
"""3D VTK viewer for non-sequential scenes.
Renders compound components and detectors as VTK actors. Optionally
overlays ray-path polylines when a SimulationResult with a
RayDatabase is provided.
The renderer registry maps compound-component types to
:class:`~ComponentRenderer3D` instances. Default renderers are
registered for Lens, Mirror, and detectors.
Attributes:
scene: The NSQScene to visualize.
_renderer_registry: Mapping from component class to renderer.
"""
def __init__(self, scene: NSQScene) -> None:
"""Initialize NSQViewer3D.
Args:
scene: The scene to visualize.
"""
super().__init__(scene)
self.scene = scene
self._renderer_registry: dict = {}
self._register_default_renderers()
def _register_default_renderers(self) -> None:
"""Register the built-in 3D renderers."""
from optiland.nonsequential.components.doublet import Doublet # noqa: PLC0415
from optiland.nonsequential.components.lens import Lens # noqa: PLC0415
from optiland.nonsequential.components.mirror import Mirror # noqa: PLC0415
from optiland.nonsequential.visualization.renderers.lens import ( # noqa: PLC0415
DoubletRenderer3D,
LensRenderer3D,
)
from optiland.nonsequential.visualization.renderers.mirror import ( # noqa: PLC0415
MirrorRenderer3D,
)
self._renderer_registry[Lens] = LensRenderer3D()
self._renderer_registry[Doublet] = DoubletRenderer3D()
self._renderer_registry[Mirror] = MirrorRenderer3D()
[docs]
def register_renderer(self, component_type: type, renderer) -> None:
"""Register a custom 3D renderer for a compound-component type.
Args:
component_type: The class to bind the renderer to.
renderer: ComponentRenderer3D instance.
"""
self._renderer_registry[component_type] = renderer
[docs]
def view(
self,
result: SimulationResult | None = None,
*,
num_rays: int = 100,
dark_mode: bool = False,
figsize: tuple[int, int] = (1200, 800),
color_by: str = "source",
) -> None:
"""Render the scene in a VTK interactive window.
When *result* is provided and contains ``ray_paths``, those paths are
used for the ray overlay without running a new trace. If *result* is
``None`` (or its ``ray_paths`` is ``None``) and ``num_rays > 0``, a
fresh trace is run internally.
Args:
result: Optional SimulationResult. If its ``ray_paths`` dict is
populated it is used for the ray overlay directly.
num_rays: Number of ray segments to overlay. Pass 0 to skip ray
drawing entirely.
dark_mode: Use a dark background if True.
figsize: (width, height) of the VTK window in pixels.
color_by: Ray colouring strategy -- ``'source'`` (default),
``'bounce'``, or ``'segment'``.
Raises:
ImportError: If VTK is not installed.
"""
import importlib.util # noqa: PLC0415
if importlib.util.find_spec("vtk") is None:
raise ImportError(
"VTK is required for 3D visualization. Install with: pip install vtk"
)
renderer = self._make_renderer(dark_mode)
# Compound components
for compound in self.scene.component_registry.compounds:
r = self._renderer_registry.get(type(compound))
if r is not None:
r.render(compound, renderer)
# Detectors
from optiland.nonsequential.visualization.renderers.detector import ( # noqa: PLC0415
DetectorRenderer3D,
)
det_r = DetectorRenderer3D()
for det in self.scene.detectors:
det_r.render(det, renderer)
if num_rays > 0:
from optiland.nonsequential.visualization.rays import (
NSQRays3D, # noqa: PLC0415
)
existing_paths = result.ray_paths if result is not None else None
rays = NSQRays3D(self.scene)
rays.plot(
renderer,
num_rays=num_rays,
color_by=color_by,
ray_paths=existing_paths,
)
window, interactor = self._make_window(
renderer, figsize, "NSQ Scene -- 3D View"
)
window.Render()
interactor.Start()