Skip to content

Mostieri/interactor layer #559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,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
Expand Down
238 changes: 235 additions & 3 deletions src/ansys/pyensight/core/utils/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,253 @@

"""


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
except ImportError:
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 _Simba:
"""Hidden class to manage the interactor layer in simba"""

def __init__(self, ensight: Union["ensight_api.ensight", "ensight"], views: "Views"):
self.ensight = ensight
self.views = views
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):
"""Initialize the data for resetting the camera."""
vport = self.ensight.objs.core.VPORTS[0]
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_look_from,
self._original_look_at,
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."""
return self.ensight.objs.core.VPORTS[0].TRANSFORMCENTER

def auto_scale(self):
"""Auto scale view."""
self.ensight.view_transf.fit()
self._initialize_simba_view()
self.render()
return self.get_camera()

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_camera(self):
"""Get EnSight camera settings in VTK format."""
vport = self.ensight.objs.core.VPORTS[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
# 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,
"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

@staticmethod
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
sinr_cosp = 2 * (w * x + y * z)
cosr_cosp = 1 - 2 * (x * x + y * y)
roll = np.arctan2(sinr_cosp, cosr_cosp)

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)

return np.degrees([roll, pitch, yaw])

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()
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]]
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
):
"""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:
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)
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, 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, invert_y, set_center
)
else:
model_point = self.ensight._session.cmd(
f"ensight.objs.core.VPORTS[0].screen_to_coords({mousex}, {mousey}, {invert_y}, {set_center})"
)
self.render()
return {"model_point": model_point, "camera": self.get_camera()}

def render(self):
self.ensight.render()
self.ensight.refresh(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._simba = _Simba(ensight, self)

# Utilities
@staticmethod
def _normalize_vector(direction: List[float]) -> List[float]:
"""Return the normalized input (3D) vector.
Expand Down Expand Up @@ -295,14 +524,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(
Expand Down
Loading