ansys-pyensight-core 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansys/pyensight/core/__init__.py +41 -0
- ansys/pyensight/core/common.py +341 -0
- ansys/pyensight/core/deep_pixel_view.html +98 -0
- ansys/pyensight/core/dockerlauncher.py +1124 -0
- ansys/pyensight/core/dvs.py +872 -0
- ansys/pyensight/core/enscontext.py +345 -0
- ansys/pyensight/core/enshell_grpc.py +641 -0
- ansys/pyensight/core/ensight_grpc.py +874 -0
- ansys/pyensight/core/ensobj.py +515 -0
- ansys/pyensight/core/launch_ensight.py +296 -0
- ansys/pyensight/core/launcher.py +388 -0
- ansys/pyensight/core/libuserd.py +2110 -0
- ansys/pyensight/core/listobj.py +280 -0
- ansys/pyensight/core/locallauncher.py +579 -0
- ansys/pyensight/core/py.typed +0 -0
- ansys/pyensight/core/renderable.py +880 -0
- ansys/pyensight/core/session.py +1923 -0
- ansys/pyensight/core/sgeo_poll.html +24 -0
- ansys/pyensight/core/utils/__init__.py +21 -0
- ansys/pyensight/core/utils/adr.py +111 -0
- ansys/pyensight/core/utils/dsg_server.py +1220 -0
- ansys/pyensight/core/utils/export.py +606 -0
- ansys/pyensight/core/utils/omniverse.py +769 -0
- ansys/pyensight/core/utils/omniverse_cli.py +614 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
- ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
- ansys/pyensight/core/utils/parts.py +1221 -0
- ansys/pyensight/core/utils/query.py +487 -0
- ansys/pyensight/core/utils/readers.py +300 -0
- ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
- ansys/pyensight/core/utils/support.py +128 -0
- ansys/pyensight/core/utils/variables.py +2019 -0
- ansys/pyensight/core/utils/views.py +674 -0
- ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
- ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
- ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
- ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
# Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
"""Views module.
|
|
24
|
+
|
|
25
|
+
The Views module allows PyEnSight to control the view in the EnSight session.
|
|
26
|
+
|
|
27
|
+
Example to set an isometric view:
|
|
28
|
+
|
|
29
|
+
::
|
|
30
|
+
(PyEnSight)
|
|
31
|
+
from ansys.pyensight.core import LocalLauncher
|
|
32
|
+
session = LocalLauncher().start()
|
|
33
|
+
views = session.ensight.utils.views
|
|
34
|
+
views.set_view_direction(1,1,1)
|
|
35
|
+
|
|
36
|
+
(EnSight)
|
|
37
|
+
from ensight.utils import views
|
|
38
|
+
views.set_view_direction(1,1,1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
import math
|
|
45
|
+
from types import ModuleType
|
|
46
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
|
47
|
+
|
|
48
|
+
import numpy as np
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
try:
|
|
52
|
+
import ensight
|
|
53
|
+
except ImportError:
|
|
54
|
+
from ansys.api.pyensight import ensight_api
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
VIEW_DICT = {
|
|
58
|
+
"x+": (1, 0, 0),
|
|
59
|
+
"x-": (-1, 0, 0),
|
|
60
|
+
"y+": (0, 1, 0),
|
|
61
|
+
"y-": (0, -1, 0),
|
|
62
|
+
"z+": (0, 0, 1),
|
|
63
|
+
"z-": (0, 0, -1),
|
|
64
|
+
"isometric": (1, 1, 1),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _Simba:
|
|
69
|
+
"""Hidden class to manage the interactor layer in simba"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, ensight: Union["ensight_api.ensight", "ensight"], views: "Views"):
|
|
72
|
+
self.ensight = ensight
|
|
73
|
+
self.views = views
|
|
74
|
+
self._original_look_at = None
|
|
75
|
+
self._original_look_from = None
|
|
76
|
+
self._original_parallel_scale = None
|
|
77
|
+
self._original_view_angle = None
|
|
78
|
+
self._original_view_up = None
|
|
79
|
+
|
|
80
|
+
def _initialize_simba_view(self):
|
|
81
|
+
"""Initialize the data for resetting the camera."""
|
|
82
|
+
vport = self.ensight.objs.core.VPORTS[0]
|
|
83
|
+
near_clip = vport.ZCLIPLIMITS[0]
|
|
84
|
+
view_angle = 2 * vport.PERSPECTIVEANGLE
|
|
85
|
+
self._original_parallel_scale = near_clip * math.tan(math.radians(view_angle) / 2)
|
|
86
|
+
self._original_view_angle = view_angle
|
|
87
|
+
(
|
|
88
|
+
self._original_look_from,
|
|
89
|
+
self._original_look_at,
|
|
90
|
+
self._original_view_up,
|
|
91
|
+
self._original_parallel_scale,
|
|
92
|
+
) = self.compute_camera_from_ensight_opengl()
|
|
93
|
+
self.ensight.annotation.axis_global("off")
|
|
94
|
+
self.ensight.annotation.axis_local("off")
|
|
95
|
+
self.ensight.annotation.axis_model("off")
|
|
96
|
+
self.ensight.view_transf.zclip_float("OFF")
|
|
97
|
+
|
|
98
|
+
def get_center_of_rotation(self):
|
|
99
|
+
"""Get EnSight center of rotation."""
|
|
100
|
+
return self.ensight.objs.core.VPORTS[0].TRANSFORMCENTER.copy()
|
|
101
|
+
|
|
102
|
+
def auto_scale(self):
|
|
103
|
+
"""Auto scale view."""
|
|
104
|
+
vport = self.ensight.objs.core.VPORTS[0]
|
|
105
|
+
vport.PERSPECTIVEANGLE = 14 # on fit we need to set a default angle.
|
|
106
|
+
self.ensight.view_transf.function("global")
|
|
107
|
+
self.ensight.view_transf.fit()
|
|
108
|
+
self._initialize_simba_view()
|
|
109
|
+
self.render()
|
|
110
|
+
return self.get_camera()
|
|
111
|
+
|
|
112
|
+
def set_view(self, value: str):
|
|
113
|
+
"""Set the view."""
|
|
114
|
+
self.ensight.view_transf.function("global")
|
|
115
|
+
if value != "isometric":
|
|
116
|
+
new_value = value[1].upper() + value[0]
|
|
117
|
+
self.ensight.view_transf.view_recall(new_value)
|
|
118
|
+
else:
|
|
119
|
+
self.views.set_view_direction(
|
|
120
|
+
1, 1, 1, perspective=self.ensight.objs.core.vports[0].PERSPECTIVE
|
|
121
|
+
)
|
|
122
|
+
return self.auto_scale()
|
|
123
|
+
|
|
124
|
+
def get_camera(self):
|
|
125
|
+
"""Get EnSight camera settings in VTK format."""
|
|
126
|
+
vport = self.ensight.objs.core.VPORTS[0]
|
|
127
|
+
position, focal_point, view_up, parallel_scale = self.compute_camera_from_ensight_opengl()
|
|
128
|
+
vport = self.ensight.objs.core.VPORTS[0]
|
|
129
|
+
view_angle = 2 * vport.PERSPECTIVEANGLE
|
|
130
|
+
# The parameter parallel scale is the actual parallel scale only
|
|
131
|
+
# if the vport is in orthographic mode. If not, it is defined as the
|
|
132
|
+
# inverge of the tangent of half of the field of view
|
|
133
|
+
parallel_scale = parallel_scale
|
|
134
|
+
return {
|
|
135
|
+
"orthographic": not vport.PERSPECTIVE,
|
|
136
|
+
"view_up": view_up,
|
|
137
|
+
"position": position,
|
|
138
|
+
"focal_point": focal_point,
|
|
139
|
+
"view_angle": view_angle,
|
|
140
|
+
"parallel_scale": parallel_scale,
|
|
141
|
+
"reset_focal_point": self._original_look_at,
|
|
142
|
+
"reset_position": self._original_look_from,
|
|
143
|
+
"reset_parallel_scale": self._original_parallel_scale,
|
|
144
|
+
"reset_view_up": self._original_view_up,
|
|
145
|
+
"reset_view_angle": self._original_view_angle,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def normalize(v):
|
|
150
|
+
"""Normalize a numpy vector."""
|
|
151
|
+
norm = np.linalg.norm(v)
|
|
152
|
+
return v / norm if norm > 0 else v
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def rotation_matrix_to_quaternion(m):
|
|
156
|
+
"""Convert a numpy rotation matrix to a quaternion."""
|
|
157
|
+
trace = np.trace(m)
|
|
158
|
+
if trace > 0:
|
|
159
|
+
s = 0.5 / np.sqrt(trace + 1.0)
|
|
160
|
+
w = 0.25 / s
|
|
161
|
+
x = (m[2, 1] - m[1, 2]) * s
|
|
162
|
+
y = (m[0, 2] - m[2, 0]) * s
|
|
163
|
+
z = (m[1, 0] - m[0, 1]) * s
|
|
164
|
+
else:
|
|
165
|
+
if m[0, 0] > m[1, 1] and m[0, 0] > m[2, 2]:
|
|
166
|
+
s = 2.0 * np.sqrt(1.0 + m[0, 0] - m[1, 1] - m[2, 2])
|
|
167
|
+
w = (m[2, 1] - m[1, 2]) / s
|
|
168
|
+
x = 0.25 * s
|
|
169
|
+
y = (m[0, 1] + m[1, 0]) / s
|
|
170
|
+
z = (m[0, 2] + m[2, 0]) / s
|
|
171
|
+
elif m[1, 1] > m[2, 2]:
|
|
172
|
+
s = 2.0 * np.sqrt(1.0 + m[1, 1] - m[0, 0] - m[2, 2])
|
|
173
|
+
w = (m[0, 2] - m[2, 0]) / s
|
|
174
|
+
x = (m[0, 1] + m[1, 0]) / s
|
|
175
|
+
y = 0.25 * s
|
|
176
|
+
z = (m[1, 2] + m[2, 1]) / s
|
|
177
|
+
else:
|
|
178
|
+
s = 2.0 * np.sqrt(1.0 + m[2, 2] - m[0, 0] - m[1, 1])
|
|
179
|
+
w = (m[1, 0] - m[0, 1]) / s
|
|
180
|
+
x = (m[0, 2] + m[2, 0]) / s
|
|
181
|
+
y = (m[1, 2] + m[2, 1]) / s
|
|
182
|
+
z = 0.25 * s
|
|
183
|
+
return np.array([x, y, z, w])
|
|
184
|
+
|
|
185
|
+
def compute_camera_from_ensight_opengl(self):
|
|
186
|
+
"""Simulate a rotating camera using the current quaternion."""
|
|
187
|
+
if isinstance(self.ensight, ModuleType):
|
|
188
|
+
data = self.ensight.objs.core.VPORTS[0].simba_camera()
|
|
189
|
+
else:
|
|
190
|
+
data = self.ensight._session.cmd("ensight.objs.core.VPORTS[0].simba_camera())")
|
|
191
|
+
camera_position = [data[0], data[1], data[2]]
|
|
192
|
+
focal_point = [data[3], data[4], data[5]]
|
|
193
|
+
view_up = [data[6], data[7], data[8]]
|
|
194
|
+
parallel_scale = 1 / data[9]
|
|
195
|
+
return camera_position, focal_point, self.views._normalize_vector(view_up), parallel_scale
|
|
196
|
+
|
|
197
|
+
def set_camera(
|
|
198
|
+
self,
|
|
199
|
+
orthographic,
|
|
200
|
+
view_up=None,
|
|
201
|
+
position=None,
|
|
202
|
+
focal_point=None,
|
|
203
|
+
view_angle=None,
|
|
204
|
+
):
|
|
205
|
+
"""Set the EnSight camera settings from the VTK input."""
|
|
206
|
+
self.ensight.view_transf.function("global")
|
|
207
|
+
perspective = "OFF" if orthographic else "ON"
|
|
208
|
+
self.ensight.view.perspective(perspective)
|
|
209
|
+
vport = self.ensight.objs.core.VPORTS[0]
|
|
210
|
+
vport.PERSPECTIVEANGLE = view_angle / 2
|
|
211
|
+
if view_up and position and focal_point:
|
|
212
|
+
current_camera = self.get_camera()
|
|
213
|
+
center = vport.TRANSFORMCENTER.copy()
|
|
214
|
+
if isinstance(self.ensight, ModuleType):
|
|
215
|
+
data = self.ensight.objs.core.VPORTS[0].simba_set_camera_helper(
|
|
216
|
+
position,
|
|
217
|
+
focal_point,
|
|
218
|
+
view_up,
|
|
219
|
+
current_camera["position"],
|
|
220
|
+
current_camera["focal_point"],
|
|
221
|
+
current_camera["view_up"],
|
|
222
|
+
center,
|
|
223
|
+
vport.ROTATION.copy(),
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
current_position = current_camera["position"]
|
|
227
|
+
current_fp = current_camera["focal_point"]
|
|
228
|
+
current_up = current_camera["view_up"]
|
|
229
|
+
cmd = "ensight.objs.core.VPORTS[0].simba_set_camera_helper("
|
|
230
|
+
cmd += f"{position}, {focal_point}, {view_up}, {current_position}, "
|
|
231
|
+
cmd += f"{current_fp}, {current_up}, {center}, "
|
|
232
|
+
cmd += f"{vport.ROTATION.copy()})"
|
|
233
|
+
data = self.ensight._session.cmd(cmd)
|
|
234
|
+
self.ensight.view_transf.rotate(data[3], data[4], data[5])
|
|
235
|
+
self.ensight.view_transf.translate(data[0], data[1], 0)
|
|
236
|
+
|
|
237
|
+
self.render()
|
|
238
|
+
return self.get_camera()
|
|
239
|
+
|
|
240
|
+
def set_perspective(self, value):
|
|
241
|
+
self.ensight.view_transf.function("global")
|
|
242
|
+
vport = self.ensight.objs.core.VPORTS[0]
|
|
243
|
+
self.ensight.view.perspective(value)
|
|
244
|
+
vport.PERSPECTIVE = value == "ON"
|
|
245
|
+
self.ensight.view_transf.zoom(1)
|
|
246
|
+
self.ensight.view_transf.rotate(0, 0, 0)
|
|
247
|
+
self.render()
|
|
248
|
+
return self.get_camera()
|
|
249
|
+
|
|
250
|
+
def screen_to_world(self, mousex, mousey, invert_y=False, set_center=False):
|
|
251
|
+
mousex = int(mousex)
|
|
252
|
+
mousey = int(mousey)
|
|
253
|
+
if isinstance(self.ensight, ModuleType):
|
|
254
|
+
model_point = self.ensight.objs.core.VPORTS[0].screen_to_coords(
|
|
255
|
+
mousex, mousey, invert_y, set_center
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
model_point = self.ensight._session.cmd(
|
|
259
|
+
f"ensight.objs.core.VPORTS[0].screen_to_coords({mousex}, {mousey}, {invert_y}, {set_center})"
|
|
260
|
+
)
|
|
261
|
+
return {"model_point": model_point, "camera": self.get_camera()}
|
|
262
|
+
|
|
263
|
+
def render(self):
|
|
264
|
+
"""Force render update in EnSight."""
|
|
265
|
+
self.ensight.refresh()
|
|
266
|
+
|
|
267
|
+
def _probe_setup(self, part_obj, get_probe_data=False):
|
|
268
|
+
self.ensight.query_interact.number_displayed(100)
|
|
269
|
+
self.ensight.query_interact.query("surface")
|
|
270
|
+
self.ensight.query_interact.display_id("OFF")
|
|
271
|
+
self.ensight.query_interact.label_always_on_top("ON")
|
|
272
|
+
self.ensight.query_interact.marker_size_normalized(2)
|
|
273
|
+
if get_probe_data:
|
|
274
|
+
variable_string = """Coordinates 'X' 'Y' 'Z'"""
|
|
275
|
+
variable_list = [variable_string]
|
|
276
|
+
variable_name = part_obj.COLORBYPALETTE
|
|
277
|
+
if variable_name:
|
|
278
|
+
if isinstance(variable_name, str):
|
|
279
|
+
variable_list.append(variable_name)
|
|
280
|
+
else:
|
|
281
|
+
if isinstance(variable_name, list):
|
|
282
|
+
if variable_name[0]:
|
|
283
|
+
variable_name = variable_name[0].DESCRIPTION
|
|
284
|
+
variable_list.append(variable_name)
|
|
285
|
+
else:
|
|
286
|
+
variable_name = None
|
|
287
|
+
if isinstance(self.ensight, ModuleType):
|
|
288
|
+
self.ensight.query_interact.select_varname_begin(*variable_list)
|
|
289
|
+
else:
|
|
290
|
+
command = "ensight.query_interact.select_varname_begin("
|
|
291
|
+
for var in variable_list:
|
|
292
|
+
command += var + ","
|
|
293
|
+
command = command[:-1] + ")"
|
|
294
|
+
self.ensight._session.cmd(command)
|
|
295
|
+
self.render()
|
|
296
|
+
|
|
297
|
+
def drag_allowed(self, mousex, mousey, invert_y=False, probe=False, get_probe_data=False):
|
|
298
|
+
"""Return True if the picked object is allowed dragging in the interactor."""
|
|
299
|
+
mousex = int(mousex)
|
|
300
|
+
mousey = int(mousey)
|
|
301
|
+
if isinstance(self.ensight, ModuleType):
|
|
302
|
+
part_id, tool_id = self.ensight.objs.core.VPORTS[0].simba_what_is_picked(
|
|
303
|
+
mousex, mousey, invert_y
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
part_id, tool_id = self.ensight._session.cmd(
|
|
307
|
+
f"ensight.objs.core.VPORTS[0].simba_what_is_picked({mousex}, {mousey}, {invert_y})"
|
|
308
|
+
)
|
|
309
|
+
coords = [None, None, None]
|
|
310
|
+
if probe:
|
|
311
|
+
screen_to_world = self.screen_to_world(
|
|
312
|
+
mousex=mousex, mousey=mousey, invert_y=invert_y, set_center=False
|
|
313
|
+
)
|
|
314
|
+
coords = screen_to_world["model_point"]
|
|
315
|
+
if tool_id > -1:
|
|
316
|
+
return True, coords[0], coords[1], coords[2], False
|
|
317
|
+
part_types_allowed = [
|
|
318
|
+
self.ensight.objs.enums.PART_CLIP_PLANE,
|
|
319
|
+
self.ensight.objs.enums.PART_ISO_SURFACE,
|
|
320
|
+
self.ensight.objs.enums.PART_CONTOUR,
|
|
321
|
+
]
|
|
322
|
+
if part_id > -1:
|
|
323
|
+
part_obj = self.ensight.objs.core.PARTS.find(part_id, "PARTNUMBER")[0]
|
|
324
|
+
if probe:
|
|
325
|
+
width, height = tuple(self.ensight.objs.core.WINDOWSIZE)
|
|
326
|
+
if invert_y:
|
|
327
|
+
mousey = height - mousey
|
|
328
|
+
self.ensight.query_interact.number_displayed(100)
|
|
329
|
+
self.ensight.query_interact.query("surface")
|
|
330
|
+
self.ensight.query_interact.display_id("OFF")
|
|
331
|
+
self.ensight.query_interact.create(mousex / width, mousey / height)
|
|
332
|
+
self._probe_setup(part_obj, get_probe_data=get_probe_data)
|
|
333
|
+
return part_obj.PARTTYPE in part_types_allowed, coords[0], coords[1], coords[2], True
|
|
334
|
+
if (
|
|
335
|
+
get_probe_data and self.ensight.objs.core.PROBES[0].PROBE_DATA
|
|
336
|
+
): # In case we have picked a probe point
|
|
337
|
+
for part in self.ensight.objs.core.PARTS:
|
|
338
|
+
self._probe_setup(part, get_probe_data=get_probe_data)
|
|
339
|
+
return False, coords[0], coords[1], coords[2], False
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class Views:
|
|
343
|
+
"""Controls the view in the current EnSight ``Session`` instance."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, ensight: Union["ensight_api.ensight", "ensight"]):
|
|
346
|
+
self.ensight = ensight
|
|
347
|
+
self._views_dict: Dict[str, Tuple[int, List[float]]] = {}
|
|
348
|
+
self._simba = _Simba(ensight, self)
|
|
349
|
+
|
|
350
|
+
@staticmethod
|
|
351
|
+
def _normalize_vector(direction: List[float]) -> List[float]:
|
|
352
|
+
"""Return the normalized input (3D) vector.
|
|
353
|
+
|
|
354
|
+
Parameters
|
|
355
|
+
----------
|
|
356
|
+
direction : list[float]
|
|
357
|
+
List representing the vector to normalize.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
list[float]
|
|
362
|
+
List representing the normalized vector.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
magnitude = math.sqrt(sum(v**2 for v in direction))
|
|
366
|
+
if magnitude == 0.0:
|
|
367
|
+
return [0] * len(direction)
|
|
368
|
+
return [x / magnitude for x in direction]
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _cross_product(vec1: List[float], vec2: List[float]) -> List[float]:
|
|
372
|
+
"""Get the cross product of two input vectors.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
vec1 : list[float]
|
|
377
|
+
List representing the first vector.
|
|
378
|
+
vec2 : list[float]
|
|
379
|
+
List representing the second vector.
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
list[float]
|
|
384
|
+
List representing the cross product of the two input vectors.
|
|
385
|
+
|
|
386
|
+
"""
|
|
387
|
+
return [
|
|
388
|
+
vec1[1] * vec2[2] - vec1[2] * vec2[1],
|
|
389
|
+
vec1[2] * vec2[0] - vec1[0] * vec2[2],
|
|
390
|
+
vec1[0] * vec2[1] - vec1[1] * vec2[0],
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
def _convert_view_direction_to_rotation_matrix(
|
|
394
|
+
self, direction: List[float], up_axis: Tuple[float, float, float] = (0.0, 1.0, 0.0)
|
|
395
|
+
) -> Tuple[List[float], List[float], List[float]]:
|
|
396
|
+
"""Convert the input direction vector in a rotation matrix.
|
|
397
|
+
|
|
398
|
+
The third row of the rotation matrix is the view direction.
|
|
399
|
+
|
|
400
|
+
The first and second rows are computed to be orthogonal to the view direction,
|
|
401
|
+
forming an orthogonal matrix. To get a specific view direction, a rotation matrix has
|
|
402
|
+
the third column, which is the view direction itself (that is the view direction becomes the z
|
|
403
|
+
axis after the rotation, while the transformed x and y axis are computed to be orthogonal to
|
|
404
|
+
the z transformed axis). The rotation is defined as the matrix transpose of the
|
|
405
|
+
defined rotation matrix because the aim is to have the view direction pointing towards the camera
|
|
406
|
+
and not the contrary.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
direction : list[float]
|
|
411
|
+
List describing the desired direction view
|
|
412
|
+
up_axis : tuple[float, float, float], optional
|
|
413
|
+
Tuple describing the up direction. The default is ``(0.0, 1.0, 0.0)``,
|
|
414
|
+
which assumes the Y axis.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
tuple
|
|
419
|
+
Tuple containing the three rows of the rotation matrix.
|
|
420
|
+
|
|
421
|
+
"""
|
|
422
|
+
direction = self._normalize_vector(direction)
|
|
423
|
+
xaxis = self._normalize_vector(self._cross_product(list(up_axis), direction))
|
|
424
|
+
yaxis = self._normalize_vector(self._cross_product(direction, xaxis))
|
|
425
|
+
return (xaxis, yaxis, direction)
|
|
426
|
+
|
|
427
|
+
def _convert_view_direction_to_quaternion(
|
|
428
|
+
self, direction: List[float], up_axis: Tuple[float, float, float] = (0.0, 1.0, 0.0)
|
|
429
|
+
) -> Tuple[float, float, float, float]:
|
|
430
|
+
"""Convert the input direction vector into a list of quaternions.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
direction : list
|
|
435
|
+
List describing the desired direction view.
|
|
436
|
+
up_axis : tuple
|
|
437
|
+
Tuple describing the up direction. The default is ``(0.0, 1.0, 0.0)``,
|
|
438
|
+
which assumes the Y axis.
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
tuple
|
|
443
|
+
Tuple containing the four quaternions describing the required rotation.
|
|
444
|
+
|
|
445
|
+
"""
|
|
446
|
+
row0, row1, row2 = self._convert_view_direction_to_rotation_matrix(
|
|
447
|
+
direction=direction,
|
|
448
|
+
up_axis=up_axis,
|
|
449
|
+
)
|
|
450
|
+
return self._convert_rotation_matrix_to_quaternion(row0, row1, row2)
|
|
451
|
+
|
|
452
|
+
def _convert_rotation_matrix_to_quaternion(
|
|
453
|
+
self, row0: List[float], row1: List[float], row2: List[float]
|
|
454
|
+
) -> Tuple[float, float, float, float]:
|
|
455
|
+
"""Convert a rotation matrix to quaternions.
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
row0 : list[float]
|
|
460
|
+
First row of the matrix.
|
|
461
|
+
row1 : list[float]
|
|
462
|
+
Second row of the matrix.
|
|
463
|
+
row2 : list[float]
|
|
464
|
+
Third row of the matrix.
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
tuple
|
|
469
|
+
Four quaternions describing the rotation.
|
|
470
|
+
|
|
471
|
+
"""
|
|
472
|
+
trace = row0[0] + row1[1] + row2[2]
|
|
473
|
+
if trace > 0:
|
|
474
|
+
s = math.sqrt(trace + 1)
|
|
475
|
+
qw = s / 2
|
|
476
|
+
s = 1 / (2 * s)
|
|
477
|
+
qx = (row2[1] - row1[2]) * s
|
|
478
|
+
qy = (row0[2] - row2[0]) * s
|
|
479
|
+
qz = (row1[0] - row0[1]) * s
|
|
480
|
+
elif row0[0] > row1[1] and row0[0] > row2[2]:
|
|
481
|
+
s = math.sqrt(1 + row0[0] - row1[1] - row2[2])
|
|
482
|
+
qx = s / 2
|
|
483
|
+
s = 1 / (2 * s)
|
|
484
|
+
qw = (row2[1] - row1[2]) * s
|
|
485
|
+
qy = (row0[1] + row1[0]) * s
|
|
486
|
+
qz = (row0[2] + row2[0]) * s
|
|
487
|
+
elif row1[1] > row2[2]:
|
|
488
|
+
s = math.sqrt(1 + row1[1] - row0[0] - row2[2])
|
|
489
|
+
qy = s / 2
|
|
490
|
+
s = 1 / (2 * s)
|
|
491
|
+
qw = (row0[2] - row2[0]) * s
|
|
492
|
+
qx = (row0[1] + row1[0]) * s
|
|
493
|
+
qz = (row1[2] + row2[1]) * s
|
|
494
|
+
else:
|
|
495
|
+
s = math.sqrt(1 + row2[2] - row0[0] - row1[1])
|
|
496
|
+
qz = s / 2
|
|
497
|
+
if s != 0.0:
|
|
498
|
+
s = 1 / (2 * s)
|
|
499
|
+
qw = (row1[0] - row0[1]) * s
|
|
500
|
+
qx = (row0[2] + row2[0]) * s
|
|
501
|
+
qy = (row1[2] + row2[1]) * s
|
|
502
|
+
list_of_quats = self._normalize_vector([qx, qy, qz, qw])
|
|
503
|
+
return list_of_quats[0], list_of_quats[1], list_of_quats[2], list_of_quats[3]
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def views_dict(self) -> Dict[str, Tuple[int, List[float]]]:
|
|
507
|
+
"""Dictionary holding the stored views.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
dict
|
|
512
|
+
Dictionary containing the stored views.
|
|
513
|
+
|
|
514
|
+
"""
|
|
515
|
+
return self._views_dict
|
|
516
|
+
|
|
517
|
+
# Methods
|
|
518
|
+
def set_center_of_transform(self, xc: float, yc: float, zc: float) -> None:
|
|
519
|
+
"""Change the center of transform of the current session.
|
|
520
|
+
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
xc : float
|
|
524
|
+
x coordinate of the new center of the transform.
|
|
525
|
+
yc : float
|
|
526
|
+
y coordinate of the new center of the transform.
|
|
527
|
+
zc : float
|
|
528
|
+
z coordinate of the new center of the transform.
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
self.ensight.view_transf.center_of_transform(xc, yc, zc)
|
|
532
|
+
|
|
533
|
+
def compute_model_centroid(self, vportindex: int = 0) -> List[float]:
|
|
534
|
+
"""Compute the model centroid using the model bounds.
|
|
535
|
+
|
|
536
|
+
Parameters
|
|
537
|
+
----------
|
|
538
|
+
vportindex : int, optional
|
|
539
|
+
Viewport to compute the centroid for. The default is ``0``.
|
|
540
|
+
|
|
541
|
+
Returns
|
|
542
|
+
-------
|
|
543
|
+
list
|
|
544
|
+
Coordinates of the model centroid.
|
|
545
|
+
|
|
546
|
+
"""
|
|
547
|
+
vport = self.ensight.objs.core.VPORTS[vportindex]
|
|
548
|
+
try:
|
|
549
|
+
# Available from release 24.1. The order is:
|
|
550
|
+
# xmin,ymin,zmin,xmax,ymax,zmax
|
|
551
|
+
bounds = vport.BOUNDINGBOX
|
|
552
|
+
xmax = bounds[3]
|
|
553
|
+
xmin = bounds[0]
|
|
554
|
+
ymax = bounds[4]
|
|
555
|
+
ymin = bounds[1]
|
|
556
|
+
zmax = bounds[5]
|
|
557
|
+
zmin = bounds[2]
|
|
558
|
+
except AttributeError: # pragma: no cover
|
|
559
|
+
# Old method. It assumes autosize is set to True and
|
|
560
|
+
# that the bounds have not been modified
|
|
561
|
+
enabled = False # pragma: no cover
|
|
562
|
+
if self.ensight.objs.core.BOUNDS is False: # pragma: no cover
|
|
563
|
+
enabled = True # pragma: no cover
|
|
564
|
+
self.ensight.view.bounds("ON") # pragma: no cover
|
|
565
|
+
xmax = vport.AXISXMAX # pragma: no cover
|
|
566
|
+
xmin = vport.AXISXMIN # pragma: no cover
|
|
567
|
+
ymax = vport.AXISYMAX # pragma: no cover
|
|
568
|
+
ymin = vport.AXISYMIN # pragma: no cover
|
|
569
|
+
zmax = vport.AXISZMAX # pragma: no cover
|
|
570
|
+
zmin = vport.AXISZMIN # pragma: no cover
|
|
571
|
+
if enabled: # pragma: no cover
|
|
572
|
+
self.ensight.view.bounds("OFF") # pragma: no cover
|
|
573
|
+
xavg = (xmax + xmin) / 2
|
|
574
|
+
yavg = (ymax + ymin) / 2
|
|
575
|
+
zavg = (zmax + zmin) / 2
|
|
576
|
+
values = [xavg, yavg, zavg]
|
|
577
|
+
return values
|
|
578
|
+
|
|
579
|
+
def set_view_direction(
|
|
580
|
+
self,
|
|
581
|
+
xdir: float,
|
|
582
|
+
ydir: float,
|
|
583
|
+
zdir: float,
|
|
584
|
+
name: Optional[str] = None,
|
|
585
|
+
perspective: Optional[bool] = False,
|
|
586
|
+
up_axis: Tuple[float, float, float] = (0.0, 1.0, 0.0),
|
|
587
|
+
vportindex: int = 0,
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Set the view direction of the session.
|
|
590
|
+
|
|
591
|
+
Parameters
|
|
592
|
+
----------
|
|
593
|
+
xdir : float
|
|
594
|
+
x component of the view direction.
|
|
595
|
+
ydir : float
|
|
596
|
+
y component of the view direction.
|
|
597
|
+
zdir : float
|
|
598
|
+
z component of the view direction.
|
|
599
|
+
name : str, optional
|
|
600
|
+
Name for the new view settings. The default is ``None``,
|
|
601
|
+
in which case an incremental name is automatically assigned.
|
|
602
|
+
perspective : bool, optional
|
|
603
|
+
Whether to enable the perspective view. The default is ``False``.
|
|
604
|
+
up_axis : tuple[float, float, float], optional
|
|
605
|
+
Up direction for the view direction. The default is ``(0.0, 1.0, 0.0)``.
|
|
606
|
+
vportindex : int, optional
|
|
607
|
+
Viewport to set the view direction for. The default is ``0``.
|
|
608
|
+
"""
|
|
609
|
+
vport = self.ensight.objs.core.VPORTS[vportindex]
|
|
610
|
+
if not perspective:
|
|
611
|
+
self.ensight.view.perspective("OFF")
|
|
612
|
+
vport.PERSPECTIVE = False
|
|
613
|
+
direction = [xdir, ydir, zdir]
|
|
614
|
+
rots = vport.ROTATION.copy()
|
|
615
|
+
rots[0:4] = self._convert_view_direction_to_quaternion(direction, up_axis=up_axis)
|
|
616
|
+
vport.ROTATION = rots
|
|
617
|
+
if perspective:
|
|
618
|
+
self.ensight.view.perspective("ON")
|
|
619
|
+
vport.PERSPECTIVE = True
|
|
620
|
+
self.save_current_view(name=name, vportindex=vportindex)
|
|
621
|
+
|
|
622
|
+
def save_current_view(
|
|
623
|
+
self,
|
|
624
|
+
name: Optional[str] = None,
|
|
625
|
+
vportindex: int = 0,
|
|
626
|
+
) -> None:
|
|
627
|
+
"""Save the current view.
|
|
628
|
+
|
|
629
|
+
Parameters
|
|
630
|
+
----------
|
|
631
|
+
name : str, optional
|
|
632
|
+
Name to give to the new view. The default is ``None``,
|
|
633
|
+
in which case an incremental name is automatically assigned.
|
|
634
|
+
vportindex : int, optional
|
|
635
|
+
Viewport to set the view direction for. The default is ``0``.
|
|
636
|
+
"""
|
|
637
|
+
vport = self.ensight.objs.core.VPORTS[vportindex]
|
|
638
|
+
coretransform = vport.CORETRANSFORM
|
|
639
|
+
if not name:
|
|
640
|
+
count = 0
|
|
641
|
+
while True and count < 100:
|
|
642
|
+
if self.views_dict.get("view_{}".format(count)):
|
|
643
|
+
count += 1
|
|
644
|
+
else:
|
|
645
|
+
self.views_dict["view_{}".format(count)] = (vportindex, coretransform)
|
|
646
|
+
break
|
|
647
|
+
else:
|
|
648
|
+
self.views_dict[name] = (vportindex, coretransform)
|
|
649
|
+
|
|
650
|
+
def restore_view(self, name: str) -> None:
|
|
651
|
+
"""Restore a stored view.
|
|
652
|
+
|
|
653
|
+
Parameters
|
|
654
|
+
----------
|
|
655
|
+
name : str
|
|
656
|
+
Name of the view to restore.
|
|
657
|
+
|
|
658
|
+
"""
|
|
659
|
+
if not self.views_dict.get(name):
|
|
660
|
+
raise KeyError("ERROR: view set not available")
|
|
661
|
+
found = self.views_dict.get(name)
|
|
662
|
+
if found: # pragma: no cover
|
|
663
|
+
viewport, coretransform = found
|
|
664
|
+
vport = self.ensight.objs.core.VPORTS[viewport]
|
|
665
|
+
vport.CORETRANSFORM = coretransform
|
|
666
|
+
|
|
667
|
+
def restore_center_of_transform(self) -> None:
|
|
668
|
+
"""Restore the center of the transform to the model centroid."""
|
|
669
|
+
original_model_centroid = self.compute_model_centroid()
|
|
670
|
+
self.set_center_of_transform(*original_model_centroid)
|
|
671
|
+
|
|
672
|
+
def reinitialize_view(self) -> None:
|
|
673
|
+
"""Reset the view."""
|
|
674
|
+
self.ensight.view_transf.initialize_viewports()
|