Source code for vifpara.views.slice

import paraview.simple as pv
import numpy as np
from typing import List

from ..logging import logger
from ..other import Vector3
from ..other.utils import intersect_vector_bounding_box
from ..base import Case, Layout
from . import IViewObject, ColorBarView, ColorMap


[docs] class Slice(IViewObject): def __init__(self, *, case: Case, origin: Vector3 = Vector3(0.0, 0.0, 1.0), normal: Vector3 = Vector3(1.0, 0.0, 0.0), camera_up: Vector3 = Vector3(0.0, 0.0, 1.0), slice_type='Plane', height=135, color_map=None, margin_x: int = 0, margin_y: int = 0, offset_x: int = 0, offset_y: int = 0, zoom=1.0, show_orientation_axis: bool = False, representation_type: str = "Surface", representation_point_size: float = 1.0): """ Initialize a Slice object used to extract and visualize a planar slice from a loaded dataset. The Slice represents a 2 dimensional rendered plane through a case. It can be used to see "inside" a case. It can be zoomed, offset, etc. :param Case case: The loaded VifPara case. :param Vector3 origin: The origin point of the slice plane. :param Vector3 normal: The normal vector defining the slice plane direction. :param Vector3 camera_up: The camera's up-vector inside the slice view. :param str slice_type: The type of slice (e.g., ``"Plane"``). :param int height: The height of the screenshot in pixels. :param ColorMap color_map: The color mapping configuration. If ``None``, the slice is displayed with a default gray color. :param int margin_x: Optional horizontal margin (positive = wider image, negative = tighter crop). :param int margin_y: Optional vertical margin (positive = taller image, negative = tighter crop). :param int offset_x: Horizontal camera shift. Positive moves the camera right, negative left. :param int offset_y: Vertical camera shift. Positive moves the camera down, negative up. :param float zoom: Zoom factor for the 3D visualization (2.0 = 2× larger, 0.5 = half size). :param bool show_orientation_axis: Whether to show the orientation axis widget. :param str representation_type: The data representation type (``"Surface"`` or ``"Points"``). :param float representation_point_size: Point size when using point-based representations. :return: None """ super().__init__(case, 263, height, show_orientation_axis) self._case = case self._origin: Vector3 = origin self._normal: Vector3 = normal self._camera_up: Vector3 = camera_up self._type: str = slice_type self._representation_type = representation_type self._representation_point_size = representation_point_size self._color_map: ColorMap = color_map self._slice_obj: pv.Slice = pv.Slice(Input=self._case.get_case()) self._color_bar: ColorBarView = ColorBarView(100, 100, self._slice_obj, color_map, slice_type) self._camera_scale: float = 1.0 / zoom self._margin_x: int = int(margin_x) self._margin_y: int = int(margin_y) self._offset_x: int = int(offset_x) self._offset_y: int = int(offset_y) self._prepare_dimensions()
[docs] @staticmethod def generate_slice_array(case: Case, generator_direction: Vector3, num_slices: int, **kwargs) -> List["Slice"]: """ Generate an array of Slice objects placed along a direction vector. This static method automatically computes a sequence of slice origins along the bounding box of the given case. It is useful for exploring datasets by quickly generating multiple evenly spaced slices through the domain. Any keyword arguments valid for the Slice constructor may be passed, except ``origin`` which is ignored because origins are auto‑generated. If ``normal`` is not provided, the ``generator_direction`` is used as the slice normal. :param Case case: The case in which slices will be generated. :param Vector3 generator_direction: The direction along which the slices are placed. If no ``normal`` is provided via kwargs, this vector will be used. :param int num_slices: The number of slices to generate. Higher values lead to finer spacing along the generator direction. :param kwargs: Additional keyword arguments forwarded to the Slice constructor (e.g., ``color_map=cmap``, ``camera_up=Vector3.forward()``). The keyword ``origin`` is not allowed and will be ignored. :return List[Slice]: A list containing all generated Slice instances. """ logger.info(f"Generating slice array in {generator_direction} direction.") case.get_case().UpdatePipeline() norm_direction = generator_direction.normalized() (x_min, x_max, y_min, y_max, z_min, z_max) = case.get_case().GetDataInformation().GetBounds() case_center: Vector3 = Vector3( (x_max + x_min) / 2, (y_max + y_min) / 2, (z_max + z_min) / 2 ) start_origin, end_origin = intersect_vector_bounding_box( case_center, generator_direction, (x_min, x_max, y_min, y_max, z_min, z_max) ) # Compute vector representing step for each slice distance: float = (end_origin - start_origin).magnitude() delta_magnitude: float = distance / num_slices delta_vector: Vector3 = norm_direction * delta_magnitude slice_array: List["Slice"] = [] # If a normal is provided, use it; otherwise use generator_direction if "normal" in kwargs: normal = kwargs["normal"] del kwargs["normal"] else: logger.info("No normal argument provided to generate_slice_array(). " "Will use generator_direction instead.") normal = generator_direction # Ignore a provided origin, because this method computes origins automatically if "origin" in kwargs: logger.warning("origin argument in generate_slice_array() is ignored. " "Slices will be generated based on the case center.") del kwargs["origin"] for i in range(num_slices): curr_origin: Vector3 = start_origin + (delta_vector * i) logger.info(f" {i}. Generating slice on {curr_origin}") new_slice: "Slice" = Slice(case=case, origin=curr_origin, normal=normal, **kwargs) slice_array.append(new_slice) return slice_array
def _render_inside(self, layout: Layout, row: int=0, col: int=0): """ Renders the slice into a cell in the layout. Parameters: ---------- layout: Layout The layout to render the slice in. row: int The row to render the slice in. col: int The col in the given row to render the slice in. """ logger.info(f"Rendering slice to <{row}|{col}>.") layout.add_render_view(row, col, self) # create slice based on origin, normal and type pv.SetActiveView(None) pv.SetActiveView(self._render_view) # add color legend if self._color_map is not None: self._color_map.apply_to_render_view(self._display, self._render_view, False) else: pv.ColorBy(self._display, None)
[docs] def set_color_bar_size(self, height: int = None, width: int = None): """ Set the size of the color bar view for the slice. If no value is provided: - ``height`` defaults to ``80`` pixels. - ``width`` defaults to the width of the slice view. :param int height: The height of the color bar in pixels. If ``None``, the default height of ``80`` px is used. :param int width: The width of the color bar in pixels. If ``None``, the width of the slice view is used. :return: None """ # Default value of height and width is None due to unification reasons of the signature. # We could set height = 80 as default, but width needs to be None to set it to the width of the slice. if height is None: height = 80 # Default number for default height (in px) of color bar if width is None: width = self._width self._color_bar.set_size(width, height)
[docs] def render_color_bar(self, layout: Layout, row: int = 0, col: int = 0): """ Render the slice's color bar into a specific cell of the layout. :param Layout layout: The layout into which the color bar should be rendered. :param int row: The target row index in the layout. :param int col: The target column index within the selected row. :return: None """ self._color_bar.set_coordinates(self._origin, self._normal) self._color_bar.render(layout, row, col)
def get_color_map(self): return self._color_map def get_color_bar(self): return self._color_bar def get_origin(self): return self._origin def get_camera_up(self): return self._camera_up def get_normal(self): return self._normal def get_render_color_view(self): return self._color_bar.get_render_view() def get_render_object(self): return self._slice_obj def _prepare_dimensions(self): # create slice based on origin, normal and type pv.SetActiveView(None) pv.SetActiveView(self._render_view) self._slice_obj.SliceType = self._type self._slice_obj.SliceType.Origin = self._origin.to_list() self._slice_obj.SliceType.Normal = self._normal.to_list() # assign slice to render-view self._display = pv.Show(self._slice_obj, self._render_view) self._display.SetRepresentationType(self._representation_type) self._display.PointSize = self._representation_point_size # compute bounds and extents (x_min, x_max, y_min, y_max, z_min, z_max) = self._slice_obj.GetDataInformation().GetBounds() slice_extends = Vector3(x_max - x_min, y_max - y_min, z_max - z_min) # base focal point (center of slice) base_focal_point = slice_extends / 2.0 + Vector3(x_min, y_min, z_min) # calculate camera coordinate system y_size = abs(self._camera_up.dot(slice_extends)) if y_size == 0: y_size = 10 y_dir = self._camera_up.normalized() x_dir = self._camera_up.cross(self._normal).normalized() x_size = abs(x_dir.dot(slice_extends)) if x_size == 0: x_size = 10 x_res = round(x_size * self._height / y_size) # apply offset in camera space offset_vector = x_dir * self._offset_x + y_dir * self._offset_y self._render_view.CameraFocalPoint = (base_focal_point + offset_vector).to_list() self._render_view.CameraPosition = (np.array(self._render_view.CameraFocalPoint) - np.array(self._normal.to_list())).tolist() self._render_view.CameraViewUp = self._camera_up.to_list() self._render_view.CameraParallelProjection = 1 # store original dimensions original_height = self._height # update with margins self._width = x_res + self._margin_x self._height += self._margin_y # scale factor to compensate for margins height_scale_factor = self._height / original_height # base camera scale base_camera_scale = y_size * 0.5 base_camera_scale *= self._camera_scale # apply compensated camera scale self._render_view.CameraParallelScale = base_camera_scale * height_scale_factor