From b60468f49b63ce1d5c939bc3a19c01d65c34d363 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Tue, 17 Jun 2025 09:30:17 +0100 Subject: [PATCH 1/9] add additional methods --- src/ansys/pyensight/core/utils/views.py | 119 ++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index e8dcd639a0a..1ad7e11b448 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -28,12 +28,34 @@ from ansys.api.pyensight import ensight_api +VIEW_DICT = { + "x+": (1, 0, 0), + "x-": (-1, 0, 0), + "y+": (0, 1, 0), + "y-": (0, -1, 0), + "z+": (0, 0, 1), + "z-": (0, 0, -1), + "isometric": (1, 1, 1), +} + + class Views: """Controls the view in the current EnSight ``Session`` instance.""" def __init__(self, ensight: Union["ensight_api.ensight", "ensight"]): self.ensight = ensight self._views_dict: Dict[str, Tuple[int, List[float]]] = {} + self._original_look_at = None + self._original_look_from = None + self._original_parallel_scale = None + + def initialize_simba_view(self): + vport = self.ensight.objs.core.VPORTS[0] + self._original_look_at = vport.LOOKATPOINT + self._original_look_from = vport.LOOKFROMPOINT + near_clip = vport.ZCLIPLIMITS[0] + view_angle = 2 * vport.PERSPECTIVEANGLE + self._original_parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) # Utilities @staticmethod @@ -358,3 +380,100 @@ def restore_center_of_transform(self) -> None: def reinitialize_view(self) -> None: """Reset the view.""" self.ensight.view_transf.initialize_viewports() + + def get_center_of_rotation(self): + """Get EnSight center of rotation.""" + self.ensight.objs.core.VPORTS[0].TRANSFORMCENTER + + def auto_scale(self): + """Auto scale view.""" + self.ensight.view_transf.fit(0) + + def set_perspective(self, value): + """Set perspective or ortographic.""" + val = "OFF" if not value else "ON" + self.ensight.view.perspective(val) + self.ensight.objs.core.VPORTS[0].PERSPECTIVE = val == "ON" + + def set_view(self, value): + """Set the view.""" + view_tuple = VIEW_DICT.get(value) + self.set_view_direction(*view_tuple) + + def _get_view_direction_from_quaternion(self, quaternion): + """Convert quaternion to view direction vector.""" + x, y, z, w = quaternion + view_direction = [2 * x * z - 2 * y * w, 2 * y * z + 2 * x * w, 1 - 2 * x**2 - 2 * y**2] + return self._normalize_vector(view_direction) + + def _get_view_up_from_quaternion(self, quaternion): + """Convert quaternion to view up vector.""" + x, y, z, w = quaternion + view_up = [2 * x * y + 2 * z * w, 1 - 2 * x**2 - 2 * z**2, 2 * y * z - 2 * x * w] + return self._normalize_vector(view_up) + + def get_camera(self): + """Get EnSight camera settings in VTK format.""" + vport = self.ensight.objs.core.VPORTS[0] + + position = vport.LOOKFROMPOINT + focal_point = vport.LOOKATPOINT + camera_dir = self._get_view_direction_from_quaternion(vport.ROTATION.copy()) + camera_right = self._normalize_vector(self._cross_product([0, 1, 0], camera_dir)) + view_up = self._normalize_vector(self._cross_product(camera_dir, camera_right)) + near_clip = vport.ZCLIPLIMITS[0] + view_angle = 2 * vport.PERSPECTIVEANGLE + parallel_scale = None + if not vport.PERSPECTIVE: + parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) + return { + "orthographic": not vport.PERSPECTIVE, + "view_up": view_up, + "position": position, + "focal_point": focal_point, + "view_angle": view_angle, + "parallel_scale": parallel_scale, + "reset_focal_point": self._original_look_at, + "reset_position": self._original_look_from, + "reset_parallel_scale": self._original_parallel_scale, + } + + def set_camera( + self, + orthographic, + view_up=None, + position=None, + focal_point=None, + view_angle=None, + parallel_scale=None, + ): + """Set the EnSight camera settings from the VTK input.""" + perspective = "OFF" if orthographic else "ON" + if orthographic: + self.ensight.view.perspective(perspective) + vport = self.ensight.objs.core.VPORTS[0] + if focal_point: + vport.LOOKATPOINT = focal_point + # vport.TRANSFORMCENTER = focal_point + if position: + vport.LOOKFROMPOINT = position + if view_angle: + vport.PERSPECTIVEANGLE = view_angle / 2 + if view_up and vport.PERSPECTIVE: + view_dir_list = self._get_view_direction_from_quaternion(vport.ROTATION.copy()) + direction = self._normalize_vector([*view_dir_list]) + right = self._cross_product(direction, view_up) + view_dir = self._normalize_vector(self._cross_product(view_up, right)) + self.set_view_direction(*view_dir, not orthographic) + if parallel_scale and not vport.PERSPECTIVE: + old_limits = vport.ZCLIPLIMITS.copy() + new_znear = parallel_scale / math.tan(math.radians(vport.PERSPECTIVEANGLE)) + vport.ZCLIPLIMITS = [new_znear, old_limits[1]] + + def get_world_coordinates(self, mousex, mousey): + """Compute the world coordinates.""" + mousex = int(mousex) + mousey = int(mousey) + return self.ensight._session.cmd( + f"ensight.objs.core.VPORTS[0].screen_to_coords({mousex}, {mousey})" + ) From 50ffe1d5b597af0838bcaab5961456b25fce4e89 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Tue, 1 Jul 2025 09:09:22 +0100 Subject: [PATCH 2/9] fix lookat and lookfrom --- src/ansys/pyensight/core/utils/views.py | 119 +++++++++++++++++------- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index 1ad7e11b448..3bf457dc226 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -38,6 +38,8 @@ "isometric": (1, 1, 1), } +DEF_CAMERA = "C0: -Z View" + class Views: """Controls the view in the current EnSight ``Session`` instance.""" @@ -48,16 +50,19 @@ def __init__(self, ensight: Union["ensight_api.ensight", "ensight"]): self._original_look_at = None self._original_look_from = None self._original_parallel_scale = None + self._original_view_angle = None + self._original_view_up = None - def initialize_simba_view(self): + def _initialize_simba_view(self): vport = self.ensight.objs.core.VPORTS[0] self._original_look_at = vport.LOOKATPOINT self._original_look_from = vport.LOOKFROMPOINT near_clip = vport.ZCLIPLIMITS[0] view_angle = 2 * vport.PERSPECTIVEANGLE self._original_parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) + self._original_view_angle = view_angle + self._original_view_up = self._get_view_up_from_quaternion(vport.ROTATION) - # Utilities @staticmethod def _normalize_vector(direction: List[float]) -> List[float]: """Return the normalized input (3D) vector. @@ -317,14 +322,17 @@ def set_view_direction( vportindex : int, optional Viewport to set the view direction for. The default is ``0``. """ - self.ensight.view.perspective("OFF") - direction = [xdir, ydir, zdir] vport = self.ensight.objs.core.VPORTS[vportindex] + if not perspective: + self.ensight.view.perspective("OFF") + vport.PERSPECTIVE = False + direction = [xdir, ydir, zdir] rots = vport.ROTATION.copy() rots[0:4] = self._convert_view_direction_to_quaternion(direction, up_axis=up_axis) vport.ROTATION = rots if perspective: self.ensight.view.perspective("ON") + vport.PERSPECTIVE = True self.save_current_view(name=name, vportindex=vportindex) def save_current_view( @@ -387,7 +395,37 @@ def get_center_of_rotation(self): def auto_scale(self): """Auto scale view.""" - self.ensight.view_transf.fit(0) + vport = self.ensight.objs.core.VPORTS[0] + bounds = vport.BOUNDINGBOX + xmax = bounds[3] + xmin = bounds[0] + ymax = bounds[4] + ymin = bounds[1] + zmax = bounds[5] + zmin = bounds[2] + low_left = [xmin, ymin, zmin] + up_right = [xmax, ymax, zmax] + dif_vec = [up_right[i] - low_left[i] for i in range(3)] + win_max = max(dif_vec) + look_at = [low_left[i] + dif_vec[i] * 0.5 for i in range(3)] + zdist = 3 * win_max / math.cos(math.radians(vport.PERSPECTIVEANGLE)) + look_from = [look_at[0], look_at[1], look_at[2] + zdist] + near_clip = vport.ZCLIPLIMITS[0] + vport = self.ensight.objs.core.VPORTS[0] + view_angle = 2 * vport.PERSPECTIVEANGLE + parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) + self.set_camera( + not vport.PERSPECTIVE, + self._get_view_up_from_quaternion(vport.ROTATION), + look_from, + look_at, + view_angle, + parallel_scale, + ) + # self.ensight.view_transf.center_of_transform(*look_from) + # self.ensight.view_transf.fit(0) + self._initialize_simba_view() + return self.get_camera() def set_perspective(self, value): """Set perspective or ortographic.""" @@ -395,10 +433,17 @@ def set_perspective(self, value): self.ensight.view.perspective(val) self.ensight.objs.core.VPORTS[0].PERSPECTIVE = val == "ON" - def set_view(self, value): + def set_view(self, value: str): """Set the view.""" - view_tuple = VIEW_DICT.get(value) - self.set_view_direction(*view_tuple) + if value != "isometric": + new_value = value[1].upper() + value[0] + self.ensight.view_transf.view_recall(new_value) + else: + self.set_view_direction( + 1, 1, 1, perspective=self.ensight.objs.core.vports[0].PERSPECTIVE + ) + self.auto_scale() + return self.get_camera() def _get_view_direction_from_quaternion(self, quaternion): """Convert quaternion to view direction vector.""" @@ -412,20 +457,31 @@ def _get_view_up_from_quaternion(self, quaternion): view_up = [2 * x * y + 2 * z * w, 1 - 2 * x**2 - 2 * z**2, 2 * y * z - 2 * x * w] return self._normalize_vector(view_up) + @staticmethod + def _get_view_up_vtk_style(view_direction): + world_up = [0, 1, 0] + dot = sum(a * b for a, b in zip(view_direction, world_up)) + if abs(dot) > 0.999: + return [0, 0, 1] + return world_up + + def get_plane_clip(self): + vport = self.ensight.objs.core.VPORTS[0] + focal_point = vport.LOOKATPOINT + mid_point = (vport.ZCLIPLIMITS[1] - vport.ZCLIPLIMITS[0]) / 2 + plane_clip = (focal_point[2] - mid_point) / abs(vport.ZCLIPLIMITS[0] - vport.ZCLIPLIMITS[1]) + return plane_clip + def get_camera(self): """Get EnSight camera settings in VTK format.""" vport = self.ensight.objs.core.VPORTS[0] - position = vport.LOOKFROMPOINT focal_point = vport.LOOKATPOINT - camera_dir = self._get_view_direction_from_quaternion(vport.ROTATION.copy()) - camera_right = self._normalize_vector(self._cross_product([0, 1, 0], camera_dir)) - view_up = self._normalize_vector(self._cross_product(camera_dir, camera_right)) + view_up = self._get_view_up_from_quaternion(vport.ROTATION) near_clip = vport.ZCLIPLIMITS[0] + vport = self.ensight.objs.core.VPORTS[0] view_angle = 2 * vport.PERSPECTIVEANGLE - parallel_scale = None - if not vport.PERSPECTIVE: - parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) + parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) return { "orthographic": not vport.PERSPECTIVE, "view_up": view_up, @@ -436,6 +492,8 @@ def get_camera(self): "reset_focal_point": self._original_look_at, "reset_position": self._original_look_from, "reset_parallel_scale": self._original_parallel_scale, + "reset_view_up": self._original_view_up, + "reset_view_angle": self._original_view_angle, } def set_camera( @@ -452,28 +510,25 @@ def set_camera( if orthographic: self.ensight.view.perspective(perspective) vport = self.ensight.objs.core.VPORTS[0] - if focal_point: - vport.LOOKATPOINT = focal_point - # vport.TRANSFORMCENTER = focal_point - if position: - vport.LOOKFROMPOINT = position if view_angle: vport.PERSPECTIVEANGLE = view_angle / 2 - if view_up and vport.PERSPECTIVE: + if view_up: view_dir_list = self._get_view_direction_from_quaternion(vport.ROTATION.copy()) direction = self._normalize_vector([*view_dir_list]) + dot = abs(sum([view_up[i] * direction[i] for i in range(3)])) + if dot > 0.999: + view_up = [0, 1, 0] if abs(direction[1]) < 0.99 else [1, 0, 0] right = self._cross_product(direction, view_up) view_dir = self._normalize_vector(self._cross_product(view_up, right)) - self.set_view_direction(*view_dir, not orthographic) - if parallel_scale and not vport.PERSPECTIVE: + self.set_view_direction(*view_dir, perspective=not orthographic) + if focal_point: + self.ensight.view_transf.look_at(*focal_point) + if position: + self.ensight.view_transf.look_from(*position) + if parallel_scale: old_limits = vport.ZCLIPLIMITS.copy() new_znear = parallel_scale / math.tan(math.radians(vport.PERSPECTIVEANGLE)) - vport.ZCLIPLIMITS = [new_znear, old_limits[1]] - - def get_world_coordinates(self, mousex, mousey): - """Compute the world coordinates.""" - mousex = int(mousex) - mousey = int(mousey) - return self.ensight._session.cmd( - f"ensight.objs.core.VPORTS[0].screen_to_coords({mousex}, {mousey})" - ) + self.ensight.view_transf.zclip_float("OFF") + self.ensight.view_transf.zclip_front(new_znear) + self.ensight.view_transf.zclip_back(old_limits[1]) + self.ensight.view_transf.zclip_float("ON") From 4712170308cfd8a1c515d30ad430fd57923d4db5 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Tue, 1 Jul 2025 17:15:34 +0100 Subject: [PATCH 3/9] working set camera --- src/ansys/pyensight/core/utils/views.py | 94 ++++++++++++++++--------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index 3bf457dc226..c772336807c 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -18,6 +18,7 @@ """ +import itertools import math from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union @@ -395,36 +396,8 @@ def get_center_of_rotation(self): def auto_scale(self): """Auto scale view.""" - vport = self.ensight.objs.core.VPORTS[0] - bounds = vport.BOUNDINGBOX - xmax = bounds[3] - xmin = bounds[0] - ymax = bounds[4] - ymin = bounds[1] - zmax = bounds[5] - zmin = bounds[2] - low_left = [xmin, ymin, zmin] - up_right = [xmax, ymax, zmax] - dif_vec = [up_right[i] - low_left[i] for i in range(3)] - win_max = max(dif_vec) - look_at = [low_left[i] + dif_vec[i] * 0.5 for i in range(3)] - zdist = 3 * win_max / math.cos(math.radians(vport.PERSPECTIVEANGLE)) - look_from = [look_at[0], look_at[1], look_at[2] + zdist] - near_clip = vport.ZCLIPLIMITS[0] - vport = self.ensight.objs.core.VPORTS[0] - view_angle = 2 * vport.PERSPECTIVEANGLE - parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) - self.set_camera( - not vport.PERSPECTIVE, - self._get_view_up_from_quaternion(vport.ROTATION), - look_from, - look_at, - view_angle, - parallel_scale, - ) - # self.ensight.view_transf.center_of_transform(*look_from) - # self.ensight.view_transf.fit(0) - self._initialize_simba_view() + self.ensight.view_transf.fit(0) + # self._initialize_simba_view() return self.get_camera() def set_perspective(self, value): @@ -496,6 +469,58 @@ def get_camera(self): "reset_view_angle": self._original_view_angle, } + @staticmethod + def identity_matrix(): + return [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + + def translation_matrix(self, tx, ty, tz): + m = self.identity_matrix() + m[3][0] = tx + m[3][1] = ty + m[3][2] = tz + return m + + @staticmethod + def multiply_4x4(a, b): + result = [[0.0 for _ in range(4)] for _ in range(4)] + for i in range(4): + for j in range(4): + for k in range(4): + result[i][j] += a[i][k] * b[k][j] + return result + + def build_rotation_matrix(self, view_dir, view_up): + # Z axis (camera forward) + z = self._normalize_vector([-v for v in view_dir]) # camera looks along -Z + + # X axis (right) + x = self._normalize_vector(self._cross_product(view_up, z)) + + # Y axis (true up) + y = self._cross_product(z, x) + # Build 4x4 rotation matrix (column-major) + rotation_matrix = [ + [-x[0], y[0], -z[0], 0], + [-x[1], y[1], -z[1], 0], + [-x[2], y[2], -z[2], 0], + [0, 0, 0, 1], + ] + + return rotation_matrix + + def apply_transform_center_to_rotation(self, rot_matrix, trans_center): + tx, ty, tz = trans_center + T = self.translation_matrix(-tx, -ty, -tz) + T_inv = self.translation_matrix(tx, ty, tz) + temp = self.multiply_4x4(T, rot_matrix) + adjusted_rot = self.multiply_4x4(temp, T_inv) + return adjusted_rot + def set_camera( self, orthographic, @@ -520,7 +545,12 @@ def set_camera( view_up = [0, 1, 0] if abs(direction[1]) < 0.99 else [1, 0, 0] right = self._cross_product(direction, view_up) view_dir = self._normalize_vector(self._cross_product(view_up, right)) - self.set_view_direction(*view_dir, perspective=not orthographic) + coretransform = vport.CORETRANSFORM.copy() + rot_mat = self.build_rotation_matrix(view_dir, view_up) + rot_mat = self.apply_transform_center_to_rotation(rot_mat, vport.TRANSFORMCENTER) + coretransform[0:16] = itertools.chain(*rot_mat.copy()) + vport.CORETRANSFORM = coretransform + # self.set_view_direction(*view_dir, perspective=not orthographic) if focal_point: self.ensight.view_transf.look_at(*focal_point) if position: @@ -528,7 +558,5 @@ def set_camera( if parallel_scale: old_limits = vport.ZCLIPLIMITS.copy() new_znear = parallel_scale / math.tan(math.radians(vport.PERSPECTIVEANGLE)) - self.ensight.view_transf.zclip_float("OFF") self.ensight.view_transf.zclip_front(new_znear) self.ensight.view_transf.zclip_back(old_limits[1]) - self.ensight.view_transf.zclip_float("ON") From 914114af5cf2b89892c2621f8d961ab9466b5def Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Mon, 7 Jul 2025 14:12:27 +0100 Subject: [PATCH 4/9] move simba stuff in its own class --- pyproject.toml | 2 +- src/ansys/pyensight/core/utils/views.py | 401 +++++++++++++----------- 2 files changed, 221 insertions(+), 182 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 466f7d3fc97..21de0352d89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,7 +160,7 @@ recursive = true exclude = ["venv/*", "tests/*"] [tool.mypy] -python_version = 3.10 +python_version = "3.10" strict = false namespace_packages = true explicit_package_bases = true diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index c772336807c..3553159c2f8 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -18,10 +18,13 @@ """ -import itertools + import math +from types import ModuleType from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union +import numpy as np + if TYPE_CHECKING: try: import ensight @@ -39,15 +42,13 @@ "isometric": (1, 1, 1), } -DEF_CAMERA = "C0: -Z View" - -class Views: - """Controls the view in the current EnSight ``Session`` instance.""" +class _Simba: + """Hidden class to manage the interactor layer in simba""" - def __init__(self, ensight: Union["ensight_api.ensight", "ensight"]): + def __init__(self, ensight: Union["ensight_api.ensight", "ensight"], views: "Views"): self.ensight = ensight - self._views_dict: Dict[str, Tuple[int, List[float]]] = {} + self.views = views self._original_look_at = None self._original_look_from = None self._original_parallel_scale = None @@ -55,14 +56,223 @@ def __init__(self, ensight: Union["ensight_api.ensight", "ensight"]): self._original_view_up = None def _initialize_simba_view(self): + """Initialize the data for resetting the camera.""" vport = self.ensight.objs.core.VPORTS[0] - self._original_look_at = vport.LOOKATPOINT - self._original_look_from = vport.LOOKFROMPOINT near_clip = vport.ZCLIPLIMITS[0] view_angle = 2 * vport.PERSPECTIVEANGLE self._original_parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) self._original_view_angle = view_angle - self._original_view_up = self._get_view_up_from_quaternion(vport.ROTATION) + ( + self._original_look_from, + self._original_look_at, + self._original_view_up, + ) = self.compute_camera_from_model_quaternion() + + def get_center_of_rotation(self): + """Get EnSight center of rotation.""" + return self.ensight.objs.core.VPORTS[0].TRANSFORMCENTER + + def auto_scale(self): + """Auto scale view.""" + self.ensight.view_transf.fit() + self._initialize_simba_view() + return self.get_camera() + + def set_perspective(self, value): + """Set perspective or ortographic.""" + val = "OFF" if not value else "ON" + self.ensight.view.perspective(val) + self.ensight.objs.core.VPORTS[0].PERSPECTIVE = val == "ON" + + def set_view(self, value: str): + """Set the view.""" + if value != "isometric": + new_value = value[1].upper() + value[0] + self.ensight.view_transf.view_recall(new_value) + else: + self.views.set_view_direction( + 1, 1, 1, perspective=self.ensight.objs.core.vports[0].PERSPECTIVE + ) + self.auto_scale() + return self.get_camera() + + def get_plane_clip(self): + """Get the depth of the current focal point.""" + vport = self.ensight.objs.core.VPORTS[0] + focal_point = self.compute_camera_from_model_quaternion()[1] + plane_clip = (focal_point[2] - vport.ZCLIPLIMITS[0]) / vport.ZCLIPLIMITS[ + 1 + ] - vport.ZCLIPLIMITS[0] + return plane_clip + + def get_camera(self): + """Get EnSight camera settings in VTK format.""" + vport = self.ensight.objs.core.VPORTS[0] + position, focal_point, view_up = self.compute_camera_from_model_quaternion() + near_clip = vport.ZCLIPLIMITS[0] + vport = self.ensight.objs.core.VPORTS[0] + view_angle = 2 * vport.PERSPECTIVEANGLE + parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) + return { + "orthographic": not vport.PERSPECTIVE, + "view_up": view_up, + "position": position, + "focal_point": focal_point, + "view_angle": view_angle, + "parallel_scale": parallel_scale, + "reset_focal_point": self._original_look_at, + "reset_position": self._original_look_from, + "reset_parallel_scale": self._original_parallel_scale, + "reset_view_up": self._original_view_up, + "reset_view_angle": self._original_view_angle, + } + + @staticmethod + def normalize(v): + """Normalize a numpy vector.""" + norm = np.linalg.norm(v) + return v / norm if norm > 0 else v + + @staticmethod + def rotation_matrix_to_quaternion(m): + """Convert a numpy rotation matrix to a quaternion.""" + trace = np.trace(m) + if trace > 0: + s = 0.5 / np.sqrt(trace + 1.0) + w = 0.25 / s + x = (m[2, 1] - m[1, 2]) * s + y = (m[0, 2] - m[2, 0]) * s + z = (m[1, 0] - m[0, 1]) * s + else: + if m[0, 0] > m[1, 1] and m[0, 0] > m[2, 2]: + s = 2.0 * np.sqrt(1.0 + m[0, 0] - m[1, 1] - m[2, 2]) + w = (m[2, 1] - m[1, 2]) / s + x = 0.25 * s + y = (m[0, 1] + m[1, 0]) / s + z = (m[0, 2] + m[2, 0]) / s + elif m[1, 1] > m[2, 2]: + s = 2.0 * np.sqrt(1.0 + m[1, 1] - m[0, 0] - m[2, 2]) + w = (m[0, 2] - m[2, 0]) / s + x = (m[0, 1] + m[1, 0]) / s + y = 0.25 * s + z = (m[1, 2] + m[2, 1]) / s + else: + s = 2.0 * np.sqrt(1.0 + m[2, 2] - m[0, 0] - m[1, 1]) + w = (m[1, 0] - m[0, 1]) / s + x = (m[0, 2] + m[2, 0]) / s + y = (m[1, 2] + m[2, 1]) / s + z = 0.25 * s + return np.array([x, y, z, w]) + + def compute_model_rotation_quaternion(self, camera_position, focal_point, view_up): + """Compute the quaternion from the input camera.""" + forward = self.normalize(np.array(focal_point) - np.array(camera_position)) + right = self.normalize(np.cross(forward, view_up)) + true_up = np.cross(right, forward) + camera_rotation = np.vstack([right, true_up, -forward]).T + model_rotation = camera_rotation.T + quat = self.rotation_matrix_to_quaternion(model_rotation) + return quat.tolist() + + @staticmethod + def quaternion_to_rotation_matrix(q): + """Convert a quaternion to a numpy rotation matrix.""" + x, y, z, w = q + return np.array( + [ + [1 - 2 * y**2 - 2 * z**2, 2 * x * y - 2 * z * w, 2 * x * z + 2 * y * w], + [2 * x * y + 2 * z * w, 1 - 2 * x**2 - 2 * z**2, 2 * y * z - 2 * x * w], + [2 * x * z - 2 * y * w, 2 * y * z + 2 * x * w, 1 - 2 * x**2 - 2 * y**2], + ] + ) + + def rotate_vector(self, v, q): + """Rotate a vector with the current quaernion.""" + R = self.quaternion_to_rotation_matrix(q) + return R @ v + + def compute_camera_from_model_quaternion(self, default_view_up=[0, 1, 0]): + """Simulate a rotating camera using the current quaternion.""" + vport = self.ensight.objs.core.VPORTS[0] + q = np.array(vport.ROTATION.copy()) + q_inv = np.array([-q[0], -q[1], -q[2], q[3]]) + + R = self.quaternion_to_rotation_matrix(q_inv) + camera_position = R @ np.array(vport.LOOKFROMPOINT) + q_inv_up = R @ np.array(default_view_up) + focal_point = self.rotate_vector(np.array(vport.LOOKATPOINT), q_inv) + return camera_position.tolist(), focal_point.tolist(), q_inv_up.tolist() + + def set_camera( + self, + orthographic, + view_up=None, + position=None, + focal_point=None, + view_angle=None, + parallel_scale=None, + ): + """Set the EnSight camera settings from the VTK input.""" + perspective = "OFF" if orthographic else "ON" + if orthographic: + self.ensight.view.perspective(perspective) + vport = self.ensight.objs.core.VPORTS[0] + if view_angle: + vport.PERSPECTIVEANGLE = view_angle / 2 + if view_up and position and focal_point: + rots = self.compute_model_rotation_quaternion(position, focal_point, view_up) + vport.ROTATION = rots + if parallel_scale: + new_znear = parallel_scale / math.tan(math.radians(vport.PERSPECTIVEANGLE)) + self.ensight.view_transf.zclip_front(new_znear) + + @staticmethod + def transform_coretransform_to_threejs(model_point, coretransform): + """Transforms a model-space point using the vport coretransform.""" + coretransform = np.array(coretransform, dtype=np.float64) + model_point = np.array(list(model_point) + [1.0]) + rotation_matrix = coretransform[0:16].reshape((4, 4)) + translation_matrix = coretransform[16:32].reshape((4, 4)) + scale_matrix = coretransform[32:48].reshape((4, 4)) + + model_matrix = translation_matrix @ rotation_matrix @ scale_matrix + world_point = model_matrix @ model_point + if world_point[3] != 1.0: + world_point[0] /= world_point[3] + world_point[1] /= world_point[3] + world_point[2] /= world_point[3] + world_point = world_point[:3] + world_point[2] *= -1 + + return world_point + + def screen_to_world(self, x, y, depth_ndc, invert_y=False): + mousex = int(x) + mousey = int(y) + vport = self.ensight.objs.core.VPORTS[0] + if isinstance(self.ensight, ModuleType): + model_point = self.ensight.objs.core.VPORTS[0].screen_to_coords( + mousex, mousey, depth_ndc, invert_y + ) + else: + model_point = self.ensight._session.cmd( + f"ensight.objs.core.VPORTS[0].screen_to_coords({mousex}, {mousey}, {depth_ndc}, {invert_y})" + ) + self.ensight.tools.cursor("ON") + self.ensight.view_transf.cursor(*model_point.copy()) + world_point = self.transform_coretransform_to_threejs( + model_point, vport.CORETRANSFORM.copy() + ) + return world_point.tolist() + + +class Views: + """Controls the view in the current EnSight ``Session`` instance.""" + + def __init__(self, ensight: Union["ensight_api.ensight", "ensight"]): + self.ensight = ensight + self._views_dict: Dict[str, Tuple[int, List[float]]] = {} + self._simba = _Simba(ensight, self) @staticmethod def _normalize_vector(direction: List[float]) -> List[float]: @@ -389,174 +599,3 @@ def restore_center_of_transform(self) -> None: def reinitialize_view(self) -> None: """Reset the view.""" self.ensight.view_transf.initialize_viewports() - - def get_center_of_rotation(self): - """Get EnSight center of rotation.""" - self.ensight.objs.core.VPORTS[0].TRANSFORMCENTER - - def auto_scale(self): - """Auto scale view.""" - self.ensight.view_transf.fit(0) - # self._initialize_simba_view() - return self.get_camera() - - def set_perspective(self, value): - """Set perspective or ortographic.""" - val = "OFF" if not value else "ON" - self.ensight.view.perspective(val) - self.ensight.objs.core.VPORTS[0].PERSPECTIVE = val == "ON" - - def set_view(self, value: str): - """Set the view.""" - if value != "isometric": - new_value = value[1].upper() + value[0] - self.ensight.view_transf.view_recall(new_value) - else: - self.set_view_direction( - 1, 1, 1, perspective=self.ensight.objs.core.vports[0].PERSPECTIVE - ) - self.auto_scale() - return self.get_camera() - - def _get_view_direction_from_quaternion(self, quaternion): - """Convert quaternion to view direction vector.""" - x, y, z, w = quaternion - view_direction = [2 * x * z - 2 * y * w, 2 * y * z + 2 * x * w, 1 - 2 * x**2 - 2 * y**2] - return self._normalize_vector(view_direction) - - def _get_view_up_from_quaternion(self, quaternion): - """Convert quaternion to view up vector.""" - x, y, z, w = quaternion - view_up = [2 * x * y + 2 * z * w, 1 - 2 * x**2 - 2 * z**2, 2 * y * z - 2 * x * w] - return self._normalize_vector(view_up) - - @staticmethod - def _get_view_up_vtk_style(view_direction): - world_up = [0, 1, 0] - dot = sum(a * b for a, b in zip(view_direction, world_up)) - if abs(dot) > 0.999: - return [0, 0, 1] - return world_up - - def get_plane_clip(self): - vport = self.ensight.objs.core.VPORTS[0] - focal_point = vport.LOOKATPOINT - mid_point = (vport.ZCLIPLIMITS[1] - vport.ZCLIPLIMITS[0]) / 2 - plane_clip = (focal_point[2] - mid_point) / abs(vport.ZCLIPLIMITS[0] - vport.ZCLIPLIMITS[1]) - return plane_clip - - def get_camera(self): - """Get EnSight camera settings in VTK format.""" - vport = self.ensight.objs.core.VPORTS[0] - position = vport.LOOKFROMPOINT - focal_point = vport.LOOKATPOINT - view_up = self._get_view_up_from_quaternion(vport.ROTATION) - near_clip = vport.ZCLIPLIMITS[0] - vport = self.ensight.objs.core.VPORTS[0] - view_angle = 2 * vport.PERSPECTIVEANGLE - parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) - return { - "orthographic": not vport.PERSPECTIVE, - "view_up": view_up, - "position": position, - "focal_point": focal_point, - "view_angle": view_angle, - "parallel_scale": parallel_scale, - "reset_focal_point": self._original_look_at, - "reset_position": self._original_look_from, - "reset_parallel_scale": self._original_parallel_scale, - "reset_view_up": self._original_view_up, - "reset_view_angle": self._original_view_angle, - } - - @staticmethod - def identity_matrix(): - return [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - - def translation_matrix(self, tx, ty, tz): - m = self.identity_matrix() - m[3][0] = tx - m[3][1] = ty - m[3][2] = tz - return m - - @staticmethod - def multiply_4x4(a, b): - result = [[0.0 for _ in range(4)] for _ in range(4)] - for i in range(4): - for j in range(4): - for k in range(4): - result[i][j] += a[i][k] * b[k][j] - return result - - def build_rotation_matrix(self, view_dir, view_up): - # Z axis (camera forward) - z = self._normalize_vector([-v for v in view_dir]) # camera looks along -Z - - # X axis (right) - x = self._normalize_vector(self._cross_product(view_up, z)) - - # Y axis (true up) - y = self._cross_product(z, x) - # Build 4x4 rotation matrix (column-major) - rotation_matrix = [ - [-x[0], y[0], -z[0], 0], - [-x[1], y[1], -z[1], 0], - [-x[2], y[2], -z[2], 0], - [0, 0, 0, 1], - ] - - return rotation_matrix - - def apply_transform_center_to_rotation(self, rot_matrix, trans_center): - tx, ty, tz = trans_center - T = self.translation_matrix(-tx, -ty, -tz) - T_inv = self.translation_matrix(tx, ty, tz) - temp = self.multiply_4x4(T, rot_matrix) - adjusted_rot = self.multiply_4x4(temp, T_inv) - return adjusted_rot - - def set_camera( - self, - orthographic, - view_up=None, - position=None, - focal_point=None, - view_angle=None, - parallel_scale=None, - ): - """Set the EnSight camera settings from the VTK input.""" - perspective = "OFF" if orthographic else "ON" - if orthographic: - self.ensight.view.perspective(perspective) - vport = self.ensight.objs.core.VPORTS[0] - if view_angle: - vport.PERSPECTIVEANGLE = view_angle / 2 - if view_up: - view_dir_list = self._get_view_direction_from_quaternion(vport.ROTATION.copy()) - direction = self._normalize_vector([*view_dir_list]) - dot = abs(sum([view_up[i] * direction[i] for i in range(3)])) - if dot > 0.999: - view_up = [0, 1, 0] if abs(direction[1]) < 0.99 else [1, 0, 0] - right = self._cross_product(direction, view_up) - view_dir = self._normalize_vector(self._cross_product(view_up, right)) - coretransform = vport.CORETRANSFORM.copy() - rot_mat = self.build_rotation_matrix(view_dir, view_up) - rot_mat = self.apply_transform_center_to_rotation(rot_mat, vport.TRANSFORMCENTER) - coretransform[0:16] = itertools.chain(*rot_mat.copy()) - vport.CORETRANSFORM = coretransform - # self.set_view_direction(*view_dir, perspective=not orthographic) - if focal_point: - self.ensight.view_transf.look_at(*focal_point) - if position: - self.ensight.view_transf.look_from(*position) - if parallel_scale: - old_limits = vport.ZCLIPLIMITS.copy() - new_znear = parallel_scale / math.tan(math.radians(vport.PERSPECTIVEANGLE)) - self.ensight.view_transf.zclip_front(new_znear) - self.ensight.view_transf.zclip_back(old_limits[1]) From 421faaa434c3566f26329ed78fdabf1348403030 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Thu, 10 Jul 2025 13:11:42 +0100 Subject: [PATCH 5/9] working projection --- src/ansys/pyensight/core/utils/views.py | 168 +++++++++++++++++++----- 1 file changed, 136 insertions(+), 32 deletions(-) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index 3553159c2f8..ef412b2cfcd 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -76,6 +76,7 @@ def auto_scale(self): """Auto scale view.""" self.ensight.view_transf.fit() self._initialize_simba_view() + self.render() return self.get_camera() def set_perspective(self, value): @@ -172,36 +173,46 @@ def compute_model_rotation_quaternion(self, camera_position, focal_point, view_u camera_rotation = np.vstack([right, true_up, -forward]).T model_rotation = camera_rotation.T quat = self.rotation_matrix_to_quaternion(model_rotation) - return quat.tolist() + return quat @staticmethod - def quaternion_to_rotation_matrix(q): - """Convert a quaternion to a numpy rotation matrix.""" + def quaternion_multiply(q1, q2): + x1, y1, z1, w1 = q1 + x2, y2, z2, w2 = q2 + w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 + x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2 + y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2 + z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2 + return np.array([x, y, z, w]) + + def quaternion_to_euler(self, q): + q = self.normalize(q) x, y, z, w = q - return np.array( - [ - [1 - 2 * y**2 - 2 * z**2, 2 * x * y - 2 * z * w, 2 * x * z + 2 * y * w], - [2 * x * y + 2 * z * w, 1 - 2 * x**2 - 2 * z**2, 2 * y * z - 2 * x * w], - [2 * x * z - 2 * y * w, 2 * y * z + 2 * x * w, 1 - 2 * x**2 - 2 * y**2], - ] - ) + sinr_cosp = 2 * (w * x + y * z) + cosr_cosp = 1 - 2 * (x * x + y * y) + roll = np.arctan2(sinr_cosp, cosr_cosp) - def rotate_vector(self, v, q): - """Rotate a vector with the current quaernion.""" - R = self.quaternion_to_rotation_matrix(q) - return R @ v + sinp = 2 * (w * y - z * x) + if abs(sinp) >= 1: + pitch = np.pi / 2 * np.sign(sinp) + else: + pitch = np.arcsin(sinp) + siny_cosp = 2 * (w * z + x * y) + cosy_cosp = 1 - 2 * (y * y + z * z) + yaw = np.arctan2(siny_cosp, cosy_cosp) - def compute_camera_from_model_quaternion(self, default_view_up=[0, 1, 0]): - """Simulate a rotating camera using the current quaternion.""" - vport = self.ensight.objs.core.VPORTS[0] - q = np.array(vport.ROTATION.copy()) - q_inv = np.array([-q[0], -q[1], -q[2], q[3]]) + return np.degrees([roll, pitch, yaw]) - R = self.quaternion_to_rotation_matrix(q_inv) - camera_position = R @ np.array(vport.LOOKFROMPOINT) - q_inv_up = R @ np.array(default_view_up) - focal_point = self.rotate_vector(np.array(vport.LOOKATPOINT), q_inv) - return camera_position.tolist(), focal_point.tolist(), q_inv_up.tolist() + def compute_camera_from_model_quaternion(self): + """Simulate a rotating camera using the current quaternion.""" + if isinstance(self.ensight, ModuleType): + data = self.ensight.objs.core.VPORTS[0].simba_camera() + else: + data = self.ensight._session.cmd("ensight.objs.core.VPORTS[0].simba_camera())") + camera_position = [data[0], data[1], data[2]] + focal_point = [data[3], data[4], data[5]] + view_up = [data[6], data[7], data[8]] + return camera_position, focal_point, self.views._normalize_vector(view_up) def set_camera( self, @@ -220,11 +231,20 @@ def set_camera( if view_angle: vport.PERSPECTIVEANGLE = view_angle / 2 if view_up and position and focal_point: - rots = self.compute_model_rotation_quaternion(position, focal_point, view_up) - vport.ROTATION = rots + q_current = self.normalize(np.array(vport.ROTATION.copy())) + q_target = self.normalize( + self.compute_model_rotation_quaternion(position, focal_point, view_up) + ) + q_relative = self.quaternion_multiply( + q_target, np.array([-q_current[0], -q_current[1], -q_current[2], q_current[3]]) + ) + angles = self.quaternion_to_euler(q_relative) + self.ensight.view_transf.rotate(*angles) if parallel_scale: - new_znear = parallel_scale / math.tan(math.radians(vport.PERSPECTIVEANGLE)) + new_znear = parallel_scale / math.tan(math.radians(view_angle / 2)) self.ensight.view_transf.zclip_front(new_znear) + self.ensight.render() + self.ensight.refresh(1) @staticmethod def transform_coretransform_to_threejs(model_point, coretransform): @@ -235,21 +255,39 @@ def transform_coretransform_to_threejs(model_point, coretransform): translation_matrix = coretransform[16:32].reshape((4, 4)) scale_matrix = coretransform[32:48].reshape((4, 4)) - model_matrix = translation_matrix @ rotation_matrix @ scale_matrix + model_matrix = translation_matrix.T @ rotation_matrix.T @ scale_matrix.T world_point = model_matrix @ model_point if world_point[3] != 1.0: world_point[0] /= world_point[3] world_point[1] /= world_point[3] world_point[2] /= world_point[3] world_point = world_point[:3] - world_point[2] *= -1 return world_point - def screen_to_world(self, x, y, depth_ndc, invert_y=False): + @staticmethod + def transform_coretransform_to_threejs_inverse(model_point, coretransform): + """Transforms a model-space point using the vport coretransform.""" + coretransform = np.array(coretransform, dtype=np.float64) + model_point = np.array(list(model_point) + [1.0]) + rotation_matrix = coretransform[0:16].reshape((4, 4)) + translation_matrix = coretransform[16:32].reshape((4, 4)) + scale_matrix = coretransform[32:48].reshape((4, 4)) + + model_matrix = translation_matrix.T @ rotation_matrix.T @ scale_matrix.T + inverse = np.linalg.inv(model_matrix) + world_point = inverse @ model_point + if world_point[3] != 1.0: + world_point[0] /= world_point[3] + world_point[1] /= world_point[3] + world_point[2] /= world_point[3] + world_point = world_point[:3] + + return world_point + + def _common(self, x, y, depth_ndc, invert_y): mousex = int(x) mousey = int(y) - vport = self.ensight.objs.core.VPORTS[0] if isinstance(self.ensight, ModuleType): model_point = self.ensight.objs.core.VPORTS[0].screen_to_coords( mousex, mousey, depth_ndc, invert_y @@ -260,11 +298,77 @@ def screen_to_world(self, x, y, depth_ndc, invert_y=False): ) self.ensight.tools.cursor("ON") self.ensight.view_transf.cursor(*model_point.copy()) - world_point = self.transform_coretransform_to_threejs( + return model_point + + def screen_to_world(self, x, y, depth_ndc, invert_y=False): + model_point = self._common(x, y, depth_ndc, invert_y) + self.ensight.view_transf.center_of_transform(*model_point) + return {"model_point": model_point, "camera": self.get_camera()} + + def screen_to_world2(self, x, y, depth_ndc, invert_y=False): + vport = self.ensight.objs.core.VPORTS[0] + model_point = self._common(x, y, depth_ndc, invert_y) + coretransform = vport.CORETRANSFORM.copy() + coretransform[48:51] = model_point.copy() + coretransform[51:54] = model_point.copy() + vport.CORETRANSFORM = coretransform + world_point = self.transform_coretransform_to_threejs_inverse( model_point, vport.CORETRANSFORM.copy() ) return world_point.tolist() + def screen_to_world3(self, x, y, invert_y=False): + position, focal_point, view_up = self.compute_camera_from_model_quaternion() + width, height = tuple(self.ensight.objs.core.WINDOWSIZE) + if invert_y: + y = height - y + model_point = self._common(x, y, 0, invert_y) + vport = self.ensight.objs.core.VPORTS[0] + depth = model_point[2] + + def look_at(eye, center, up): + f = self.normalize(center - eye) + s = self.normalize(np.cross(f, up)) + u = np.cross(s, f) + view = np.identity(4) + view[0, :3] = s + view[1, :3] = u + view[2, :3] = -f + view[:3, 3] = -view[:3, :3] @ eye + return view + + def perspective(fov_y, aspect, near, far): + f = 1.0 / np.tan(np.radians(fov_y) / 2) + proj = np.zeros((4, 4)) + proj[0, 0] = f / aspect + proj[1, 1] = f + proj[2, 2] = (far + near) / (near - far) + proj[2, 3] = (2 * far * near) / (near - far) + proj[3, 2] = -1.0 + return proj + + eye = np.array(position, dtype=np.float64) + center = np.array(focal_point, dtype=np.float64) + up = np.array(view_up, dtype=np.float64) + aspect = width / height + view_matrix = look_at(eye, center, up) + near, far = tuple(vport.ZCLIPLIMITS.copy()) + projection_matrix = perspective(vport.PERSPECTIVEANGLE * 2, aspect, near, far) + inv_proj_view = np.linalg.inv(projection_matrix @ view_matrix) + x_ndc = (2.0 * x) / width - 1.0 + y_ndc = 1.0 - (2.0 * y) / height # flip Y + z_ndc = depth + + ndc = np.array([x_ndc, y_ndc, z_ndc, 1.0]) + world = inv_proj_view @ ndc + world /= world[3] + + return world[:3] + + def render(self): + self.ensight.render() + self.ensight.refresh(1) + class Views: """Controls the view in the current EnSight ``Session`` instance.""" From bc1b914de39647a4f2a584fb17bb47e99b0dc182 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Thu, 10 Jul 2025 13:13:16 +0100 Subject: [PATCH 6/9] clean unused functions --- src/ansys/pyensight/core/utils/views.py | 104 +----------------------- 1 file changed, 1 insertion(+), 103 deletions(-) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index ef412b2cfcd..b340412e296 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -246,46 +246,7 @@ def set_camera( self.ensight.render() self.ensight.refresh(1) - @staticmethod - def transform_coretransform_to_threejs(model_point, coretransform): - """Transforms a model-space point using the vport coretransform.""" - coretransform = np.array(coretransform, dtype=np.float64) - model_point = np.array(list(model_point) + [1.0]) - rotation_matrix = coretransform[0:16].reshape((4, 4)) - translation_matrix = coretransform[16:32].reshape((4, 4)) - scale_matrix = coretransform[32:48].reshape((4, 4)) - - model_matrix = translation_matrix.T @ rotation_matrix.T @ scale_matrix.T - world_point = model_matrix @ model_point - if world_point[3] != 1.0: - world_point[0] /= world_point[3] - world_point[1] /= world_point[3] - world_point[2] /= world_point[3] - world_point = world_point[:3] - - return world_point - - @staticmethod - def transform_coretransform_to_threejs_inverse(model_point, coretransform): - """Transforms a model-space point using the vport coretransform.""" - coretransform = np.array(coretransform, dtype=np.float64) - model_point = np.array(list(model_point) + [1.0]) - rotation_matrix = coretransform[0:16].reshape((4, 4)) - translation_matrix = coretransform[16:32].reshape((4, 4)) - scale_matrix = coretransform[32:48].reshape((4, 4)) - - model_matrix = translation_matrix.T @ rotation_matrix.T @ scale_matrix.T - inverse = np.linalg.inv(model_matrix) - world_point = inverse @ model_point - if world_point[3] != 1.0: - world_point[0] /= world_point[3] - world_point[1] /= world_point[3] - world_point[2] /= world_point[3] - world_point = world_point[:3] - - return world_point - - def _common(self, x, y, depth_ndc, invert_y): + def screen_to_world(self, x, y, depth_ndc, invert_y=False): mousex = int(x) mousey = int(y) if isinstance(self.ensight, ModuleType): @@ -298,73 +259,10 @@ def _common(self, x, y, depth_ndc, invert_y): ) self.ensight.tools.cursor("ON") self.ensight.view_transf.cursor(*model_point.copy()) - return model_point - - def screen_to_world(self, x, y, depth_ndc, invert_y=False): model_point = self._common(x, y, depth_ndc, invert_y) self.ensight.view_transf.center_of_transform(*model_point) return {"model_point": model_point, "camera": self.get_camera()} - def screen_to_world2(self, x, y, depth_ndc, invert_y=False): - vport = self.ensight.objs.core.VPORTS[0] - model_point = self._common(x, y, depth_ndc, invert_y) - coretransform = vport.CORETRANSFORM.copy() - coretransform[48:51] = model_point.copy() - coretransform[51:54] = model_point.copy() - vport.CORETRANSFORM = coretransform - world_point = self.transform_coretransform_to_threejs_inverse( - model_point, vport.CORETRANSFORM.copy() - ) - return world_point.tolist() - - def screen_to_world3(self, x, y, invert_y=False): - position, focal_point, view_up = self.compute_camera_from_model_quaternion() - width, height = tuple(self.ensight.objs.core.WINDOWSIZE) - if invert_y: - y = height - y - model_point = self._common(x, y, 0, invert_y) - vport = self.ensight.objs.core.VPORTS[0] - depth = model_point[2] - - def look_at(eye, center, up): - f = self.normalize(center - eye) - s = self.normalize(np.cross(f, up)) - u = np.cross(s, f) - view = np.identity(4) - view[0, :3] = s - view[1, :3] = u - view[2, :3] = -f - view[:3, 3] = -view[:3, :3] @ eye - return view - - def perspective(fov_y, aspect, near, far): - f = 1.0 / np.tan(np.radians(fov_y) / 2) - proj = np.zeros((4, 4)) - proj[0, 0] = f / aspect - proj[1, 1] = f - proj[2, 2] = (far + near) / (near - far) - proj[2, 3] = (2 * far * near) / (near - far) - proj[3, 2] = -1.0 - return proj - - eye = np.array(position, dtype=np.float64) - center = np.array(focal_point, dtype=np.float64) - up = np.array(view_up, dtype=np.float64) - aspect = width / height - view_matrix = look_at(eye, center, up) - near, far = tuple(vport.ZCLIPLIMITS.copy()) - projection_matrix = perspective(vport.PERSPECTIVEANGLE * 2, aspect, near, far) - inv_proj_view = np.linalg.inv(projection_matrix @ view_matrix) - x_ndc = (2.0 * x) / width - 1.0 - y_ndc = 1.0 - (2.0 * y) / height # flip Y - z_ndc = depth - - ndc = np.array([x_ndc, y_ndc, z_ndc, 1.0]) - world = inv_proj_view @ ndc - world /= world[3] - - return world[:3] - def render(self): self.ensight.render() self.ensight.refresh(1) From 926dc4fef5b2f0af10efe92d2a20ba78baef1f34 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Thu, 10 Jul 2025 13:13:38 +0100 Subject: [PATCH 7/9] return only model point --- src/ansys/pyensight/core/utils/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index b340412e296..006896a7d57 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -261,7 +261,7 @@ def screen_to_world(self, x, y, depth_ndc, invert_y=False): self.ensight.view_transf.cursor(*model_point.copy()) model_point = self._common(x, y, depth_ndc, invert_y) self.ensight.view_transf.center_of_transform(*model_point) - return {"model_point": model_point, "camera": self.get_camera()} + return model_point def render(self): self.ensight.render() From 4bc038561b0af408d0f78e8a6f4e81508f66fa67 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Fri, 11 Jul 2025 11:44:41 +0100 Subject: [PATCH 8/9] final version --- src/ansys/pyensight/core/utils/views.py | 72 ++++++++++--------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index 006896a7d57..fda4e8c165a 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -66,7 +66,8 @@ def _initialize_simba_view(self): self._original_look_from, self._original_look_at, self._original_view_up, - ) = self.compute_camera_from_model_quaternion() + self._original_parallel_scale, + ) = self.compute_camera_from_ensight_opengl() def get_center_of_rotation(self): """Get EnSight center of rotation.""" @@ -79,12 +80,6 @@ def auto_scale(self): self.render() return self.get_camera() - def set_perspective(self, value): - """Set perspective or ortographic.""" - val = "OFF" if not value else "ON" - self.ensight.view.perspective(val) - self.ensight.objs.core.VPORTS[0].PERSPECTIVE = val == "ON" - def set_view(self, value: str): """Set the view.""" if value != "isometric": @@ -97,23 +92,16 @@ def set_view(self, value: str): self.auto_scale() return self.get_camera() - def get_plane_clip(self): - """Get the depth of the current focal point.""" - vport = self.ensight.objs.core.VPORTS[0] - focal_point = self.compute_camera_from_model_quaternion()[1] - plane_clip = (focal_point[2] - vport.ZCLIPLIMITS[0]) / vport.ZCLIPLIMITS[ - 1 - ] - vport.ZCLIPLIMITS[0] - return plane_clip - def get_camera(self): """Get EnSight camera settings in VTK format.""" vport = self.ensight.objs.core.VPORTS[0] - position, focal_point, view_up = self.compute_camera_from_model_quaternion() - near_clip = vport.ZCLIPLIMITS[0] + position, focal_point, view_up, parallel_scale = self.compute_camera_from_ensight_opengl() vport = self.ensight.objs.core.VPORTS[0] view_angle = 2 * vport.PERSPECTIVEANGLE - parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2) + # The parameter parallel scale is the actual parallel scale only + # if the vport is in orthographic mode. If not, it is defined as the + # inverge of the tangent of half of the field of view + parallel_scale = parallel_scale return { "orthographic": not vport.PERSPECTIVE, "view_up": view_up, @@ -203,7 +191,7 @@ def quaternion_to_euler(self, q): return np.degrees([roll, pitch, yaw]) - def compute_camera_from_model_quaternion(self): + def compute_camera_from_ensight_opengl(self): """Simulate a rotating camera using the current quaternion.""" if isinstance(self.ensight, ModuleType): data = self.ensight.objs.core.VPORTS[0].simba_camera() @@ -212,16 +200,11 @@ def compute_camera_from_model_quaternion(self): camera_position = [data[0], data[1], data[2]] focal_point = [data[3], data[4], data[5]] view_up = [data[6], data[7], data[8]] - return camera_position, focal_point, self.views._normalize_vector(view_up) + parallel_scale = 1 / data[9] + return camera_position, focal_point, self.views._normalize_vector(view_up), parallel_scale def set_camera( - self, - orthographic, - view_up=None, - position=None, - focal_point=None, - view_angle=None, - parallel_scale=None, + self, orthographic, view_up=None, position=None, focal_point=None, view_angle=None ): """Set the EnSight camera settings from the VTK input.""" perspective = "OFF" if orthographic else "ON" @@ -230,6 +213,7 @@ def set_camera( vport = self.ensight.objs.core.VPORTS[0] if view_angle: vport.PERSPECTIVEANGLE = view_angle / 2 + if view_up and position and focal_point: q_current = self.normalize(np.array(vport.ROTATION.copy())) q_target = self.normalize( @@ -240,28 +224,30 @@ def set_camera( ) angles = self.quaternion_to_euler(q_relative) self.ensight.view_transf.rotate(*angles) - if parallel_scale: - new_znear = parallel_scale / math.tan(math.radians(view_angle / 2)) - self.ensight.view_transf.zclip_front(new_znear) - self.ensight.render() - self.ensight.refresh(1) + self.render() + + def set_perspective(self, value): + vport = self.ensight.objs.core.VPORTS[0] + self.ensight.view.perspective(value) + vport.PERSPECTIVE = value == "ON" + self.ensight.view_transf.zoom(1) + self.ensight.view_transf.rotate(0, 0, 0) + self.render() + return self.get_camera() - def screen_to_world(self, x, y, depth_ndc, invert_y=False): - mousex = int(x) - mousey = int(y) + def screen_to_world(self, mousex, mousey, invert_y=False, set_center=False): + mousex = int(mousex) + mousey = int(mousey) if isinstance(self.ensight, ModuleType): model_point = self.ensight.objs.core.VPORTS[0].screen_to_coords( - mousex, mousey, depth_ndc, invert_y + mousex, mousey, invert_y, set_center ) else: model_point = self.ensight._session.cmd( - f"ensight.objs.core.VPORTS[0].screen_to_coords({mousex}, {mousey}, {depth_ndc}, {invert_y})" + f"ensight.objs.core.VPORTS[0].screen_to_coords({mousex}, {mousey}, {invert_y}, {set_center})" ) - self.ensight.tools.cursor("ON") - self.ensight.view_transf.cursor(*model_point.copy()) - model_point = self._common(x, y, depth_ndc, invert_y) - self.ensight.view_transf.center_of_transform(*model_point) - return model_point + self.render() + return {"model_point": model_point, "camera": self.get_camera()} def render(self): self.ensight.render() From b01016b7cf59856275ece4d443b6f7e82e576aa1 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Mon, 14 Jul 2025 14:31:25 +0100 Subject: [PATCH 9/9] disable ensight axis --- src/ansys/pyensight/core/utils/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ansys/pyensight/core/utils/views.py b/src/ansys/pyensight/core/utils/views.py index fda4e8c165a..f7fe6e1e879 100644 --- a/src/ansys/pyensight/core/utils/views.py +++ b/src/ansys/pyensight/core/utils/views.py @@ -68,6 +68,9 @@ def _initialize_simba_view(self): self._original_view_up, self._original_parallel_scale, ) = self.compute_camera_from_ensight_opengl() + self.ensight.annotation.axis_global("off") + self.ensight.annotation.axis_local("off") + self.ensight.annotation.axis_model("off") def get_center_of_rotation(self): """Get EnSight center of rotation."""