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.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. 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()