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,880 @@
|
|
|
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
|
+
"""Renderable module.
|
|
24
|
+
|
|
25
|
+
This module provides the interface for creating objects in the EnSight session
|
|
26
|
+
that can be displayed via HTML over the websocket server interface.
|
|
27
|
+
"""
|
|
28
|
+
import hashlib
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, no_type_check
|
|
32
|
+
import uuid
|
|
33
|
+
import warnings
|
|
34
|
+
import webbrowser
|
|
35
|
+
|
|
36
|
+
import requests
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from ansys.pyensight.core import Session
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_ansysnexus_version(version: Union[int, str]) -> str:
|
|
43
|
+
if int(version) < 242:
|
|
44
|
+
return ""
|
|
45
|
+
return str(version)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Renderable:
|
|
49
|
+
"""Generates HTML pages for renderable entities.
|
|
50
|
+
|
|
51
|
+
This class provides the underlying HTML remote webpage generation for
|
|
52
|
+
the :func:`show<ansys.pyensight.core.Session.show>` method. The approach
|
|
53
|
+
is to generate the renderable in the EnSight session and make the
|
|
54
|
+
artifacts available via the websocket server. The artifacts are then
|
|
55
|
+
wrapped with simple HTML pages, which are also served up by the websocket
|
|
56
|
+
server. These HTML pages can then be used to populate iframes.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
session :
|
|
61
|
+
PyEnSight session to generate renderables for.
|
|
62
|
+
cell_handle :
|
|
63
|
+
Jupyter notebook cell handle (if any). The default is ``None``.
|
|
64
|
+
width : int, optional
|
|
65
|
+
Width of the renderable. The default is ``None``.
|
|
66
|
+
height : int, optional
|
|
67
|
+
Height of the renderable. The default is ``None``.
|
|
68
|
+
temporal : bool, optional
|
|
69
|
+
Whether to show data for all timesteps in an interactive
|
|
70
|
+
WebGL-based browser viewer. The default is ``False``.
|
|
71
|
+
aa : int, optional
|
|
72
|
+
Number of antialiasing passes to use when rendering images.
|
|
73
|
+
The default is ``1``.
|
|
74
|
+
fps : float, optional
|
|
75
|
+
Number of frames per second to use for animation playback. The
|
|
76
|
+
default is ``30.0``.
|
|
77
|
+
num_frames : int, optional
|
|
78
|
+
Number of frames of static timestep to record for animation playback.
|
|
79
|
+
The default is ``None``.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
session: "Session",
|
|
86
|
+
cell_handle: Optional[Any] = None,
|
|
87
|
+
width: Optional[int] = None,
|
|
88
|
+
height: Optional[int] = None,
|
|
89
|
+
temporal: bool = False,
|
|
90
|
+
aa: int = 1,
|
|
91
|
+
fps: float = 30.0,
|
|
92
|
+
num_frames: Optional[int] = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
self._session = session
|
|
95
|
+
self._filename_index: int = 0
|
|
96
|
+
self._guid: str = str(uuid.uuid1()).replace("-", "")
|
|
97
|
+
self._download_names: List[str] = []
|
|
98
|
+
# The Jupyter notebook cell handle (if any)
|
|
99
|
+
self._cell_handle = cell_handle
|
|
100
|
+
# the URL to the base HTML file for this entity
|
|
101
|
+
self._url: Optional[str] = None
|
|
102
|
+
# the pathname of the HTML file in the remote EnSight session
|
|
103
|
+
self._url_remote_pathname: Optional[str] = None
|
|
104
|
+
# the name passed to the pyensight session show() string
|
|
105
|
+
self._rendertype: str = ""
|
|
106
|
+
# Common attributes used by various subclasses
|
|
107
|
+
self._width: Optional[int] = width
|
|
108
|
+
self._height: Optional[int] = height
|
|
109
|
+
self._temporal: bool = temporal
|
|
110
|
+
self._aa: int = aa
|
|
111
|
+
self._fps: float = fps
|
|
112
|
+
self._num_frames: Optional[int] = num_frames
|
|
113
|
+
#
|
|
114
|
+
self._using_proxy = False
|
|
115
|
+
# if we're talking directly to WS, then use 'http' otherwise 'https' for the proxy
|
|
116
|
+
self._http_protocol = "http"
|
|
117
|
+
try:
|
|
118
|
+
if self._session.launcher._pim_instance is not None:
|
|
119
|
+
self._using_proxy = True
|
|
120
|
+
self._http_protocol = "https"
|
|
121
|
+
except Exception:
|
|
122
|
+
# the launcher may not be PIM aware; that's ok
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
name = self.__class__.__name__
|
|
127
|
+
return f"{name}( url='{self._url}' )"
|
|
128
|
+
|
|
129
|
+
@no_type_check
|
|
130
|
+
def _repr_pretty_(self, p: "pretty", cycle: bool) -> None:
|
|
131
|
+
"""Support the pretty module for better IPython support.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
p : text, optional
|
|
136
|
+
Pretty flag. The default is ``"pretty"``.
|
|
137
|
+
cycle : bool, optional
|
|
138
|
+
Cycle flag. The default is ``None``.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
name = self.__class__.__name__
|
|
142
|
+
p.text(f"{name}( url='{self._url}' )")
|
|
143
|
+
|
|
144
|
+
def _get_query_parameters_str(self, params: Optional[Dict[str, str]] = None) -> str:
|
|
145
|
+
"""Generate any optional http query parameters.
|
|
146
|
+
Return a string formatted as a URL query to tack on to the
|
|
147
|
+
beginning part of the URL. The string may be empty if there
|
|
148
|
+
aren't any parameters. The method takes a dict
|
|
149
|
+
of parameters, possibly empty or None, and combines it with the
|
|
150
|
+
parameters from the launcher, which also may be empty.
|
|
151
|
+
"""
|
|
152
|
+
qp_dict = self._session.launcher._get_query_parameters()
|
|
153
|
+
if qp_dict is None:
|
|
154
|
+
# just in case
|
|
155
|
+
qp_dict = {}
|
|
156
|
+
if params:
|
|
157
|
+
qp_dict.update(params)
|
|
158
|
+
query_parameter_str = ""
|
|
159
|
+
symbol = "?"
|
|
160
|
+
for p in qp_dict.items():
|
|
161
|
+
query_parameter_str += f"{symbol}{p[0]}={p[1]}"
|
|
162
|
+
symbol = "&"
|
|
163
|
+
return query_parameter_str
|
|
164
|
+
|
|
165
|
+
def _generate_filename(self, suffix: str) -> Tuple[str, str]:
|
|
166
|
+
"""Create session-specific files and URLs.
|
|
167
|
+
|
|
168
|
+
Every time this method is called, a new filename (on the EnSight host)
|
|
169
|
+
and the associated URL for that file are generated. The caller
|
|
170
|
+
provides the suffix for the names.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
suffix: str
|
|
175
|
+
Suffix of the file.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
Tuple[str, str]
|
|
180
|
+
Filename to use on the host system and the URL that accesses the
|
|
181
|
+
file via REST calls to the websocket server.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
filename = f"{self._session.secret_key}_{self._guid}_{self._filename_index}{suffix}"
|
|
185
|
+
# Note: cannot use os.path.join here as the OS of the EnSight session might not match
|
|
186
|
+
# the client OS.
|
|
187
|
+
pathname = f"{self._session.launcher.session_directory}/{filename}"
|
|
188
|
+
self._filename_index += 1
|
|
189
|
+
return pathname, filename
|
|
190
|
+
|
|
191
|
+
def _generate_url(self) -> None:
|
|
192
|
+
"""Build the remote HTML filename and associated URL.
|
|
193
|
+
|
|
194
|
+
On the remote system the, pathname to the HTML file is
|
|
195
|
+
``{session_directory}/{session}_{guid}_{index}_{type}.html``.
|
|
196
|
+
The URL to the file (through the session HTTP server) is
|
|
197
|
+
``http://{system}:{websocketserverhtmlport}/{session}_{guid}_{index}_{type}.html``.
|
|
198
|
+
|
|
199
|
+
Note that there may be optional http query parameters at the end of the URL.
|
|
200
|
+
|
|
201
|
+
After this call, ``_url`` and ``_url_remote_pathname`` reflect these names.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
suffix = f"_{self._rendertype}.html"
|
|
205
|
+
filename_index = self._filename_index
|
|
206
|
+
remote_pathname, _ = self._generate_filename(suffix)
|
|
207
|
+
simple_filename = f"{self._session.secret_key}_{self._guid}_{filename_index}{suffix}"
|
|
208
|
+
url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
|
|
209
|
+
self._url = f"{url}/{simple_filename}{self._get_query_parameters_str()}"
|
|
210
|
+
self._url_remote_pathname = remote_pathname
|
|
211
|
+
|
|
212
|
+
def _save_remote_html_page(self, html: str) -> None:
|
|
213
|
+
"""Create an HTML webpage on the remote host.
|
|
214
|
+
|
|
215
|
+
Given a snippet of HTML, create a file on the remote server with the
|
|
216
|
+
name generated by the ``_generate_url()`` method.
|
|
217
|
+
The most common use is to generate an "iframe" wrapper around some HTML
|
|
218
|
+
snippet.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
html : str
|
|
223
|
+
HTML snippet to wrap remotely.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
# save "html" into a file on the remote server with filename .html
|
|
227
|
+
cmd = f'open(r"""{self._url_remote_pathname}""", "w").write("""{html}""")'
|
|
228
|
+
self._session.grpc.command(cmd, do_eval=False)
|
|
229
|
+
|
|
230
|
+
def browser(self) -> None:
|
|
231
|
+
"""Open a web browser page to display the renderable content."""
|
|
232
|
+
if self._url:
|
|
233
|
+
webbrowser.open(self._url)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def url(self) -> Optional[str]:
|
|
237
|
+
"""URL to the renderable content."""
|
|
238
|
+
return self._url
|
|
239
|
+
|
|
240
|
+
def _default_size(self, width: int, height: int) -> Tuple[int, int]:
|
|
241
|
+
"""Propose and return a size for a rectangle.
|
|
242
|
+
|
|
243
|
+
The renderable may have been constructed with user-supplied width and height
|
|
244
|
+
information. If so, that information is returned. If not, the width and
|
|
245
|
+
height values passed to this method are returned.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
width : int
|
|
250
|
+
Width value to return if the renderable does not have a width.
|
|
251
|
+
height : int
|
|
252
|
+
Height value to return if the renderable does not have a height.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
Tuple[int, int]
|
|
257
|
+
Tuple (width, height) of the size values to use.
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
out_w = self._width
|
|
261
|
+
if out_w is None:
|
|
262
|
+
out_w = width
|
|
263
|
+
out_h = self._height
|
|
264
|
+
if out_h is None:
|
|
265
|
+
out_h = height
|
|
266
|
+
return out_w, out_h
|
|
267
|
+
|
|
268
|
+
def update(self) -> None:
|
|
269
|
+
"""Update the visualization and display it.
|
|
270
|
+
|
|
271
|
+
When this method is called, the graphics content is updated to the
|
|
272
|
+
current EnSight instance state. For example, an image might be re-captured.
|
|
273
|
+
The URL of the content stays the same, but the content that the URL displays is
|
|
274
|
+
updated.
|
|
275
|
+
|
|
276
|
+
If the renderable was created in the context of a Jupyter notebook cell,
|
|
277
|
+
the original cell display is updated.
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
if self._cell_handle:
|
|
281
|
+
from IPython.display import IFrame
|
|
282
|
+
|
|
283
|
+
width, height = self._default_size(800, 600)
|
|
284
|
+
self._cell_handle.update(IFrame(src=self._url, width=width, height=height))
|
|
285
|
+
|
|
286
|
+
def delete(self) -> None:
|
|
287
|
+
"""Delete all server resources for the renderable.
|
|
288
|
+
|
|
289
|
+
A renderable occupies resources in the EnSight :class:`Session<ansys.pyensight.core.Session>`
|
|
290
|
+
instance. This method releases those resources. Once this method is called, the renderable
|
|
291
|
+
can no longer be displayed.
|
|
292
|
+
|
|
293
|
+
Notes
|
|
294
|
+
-----
|
|
295
|
+
This method has not yet been implemented.
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
def download(self, dirname: str) -> List[str]:
|
|
301
|
+
"""Download the content files for the renderable.
|
|
302
|
+
|
|
303
|
+
A renderable saves files (such as images, mpegs, and geometry) in the EnSight instance.
|
|
304
|
+
Normally, these files are accessed via the webpage specified in the URL property.
|
|
305
|
+
This method allows for those files to be downloaded to a local directory so that they
|
|
306
|
+
can be used for other purposes.
|
|
307
|
+
|
|
308
|
+
.. note::
|
|
309
|
+
Any previously existing files with the same name are overwritten.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
dirname : str
|
|
314
|
+
Name of the existing directory to save the files to.
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
list
|
|
320
|
+
List of names for the downloaded files.
|
|
321
|
+
|
|
322
|
+
Examples
|
|
323
|
+
--------
|
|
324
|
+
Download the PNG file generated by the image renderable.
|
|
325
|
+
|
|
326
|
+
>>> img = session.show('image", width=640, height=480, aa=4)
|
|
327
|
+
>>> names = img.download("/tmp")
|
|
328
|
+
>>> png_pathname = os.path.join("/tmp", names[0])
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
for filename in self._download_names:
|
|
332
|
+
url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}/{filename}{self._get_query_parameters_str()}"
|
|
333
|
+
outpath = os.path.join(dirname, filename)
|
|
334
|
+
with requests.get(url, stream=True) as r:
|
|
335
|
+
with open(outpath, "wb") as f:
|
|
336
|
+
shutil.copyfileobj(r.raw, f)
|
|
337
|
+
return self._download_names
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class RenderableImage(Renderable):
|
|
341
|
+
"""Renders an image on the EnSight host system and makes it available via a webpage."""
|
|
342
|
+
|
|
343
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
344
|
+
"""Initialize RenderableImage."""
|
|
345
|
+
super().__init__(*args, **kwargs)
|
|
346
|
+
self._rendertype = "image"
|
|
347
|
+
self._generate_url()
|
|
348
|
+
# the HTML serves up a PNG file
|
|
349
|
+
pathname, filename = self._generate_filename(".png")
|
|
350
|
+
self._png_pathname = pathname
|
|
351
|
+
self._png_filename = filename
|
|
352
|
+
# the download is the png file
|
|
353
|
+
self._download_names.append(self._png_filename)
|
|
354
|
+
self.update()
|
|
355
|
+
|
|
356
|
+
def update(self):
|
|
357
|
+
"""Update the image and display it.
|
|
358
|
+
|
|
359
|
+
If the renderable is part of a Jupyter notebook cell, that cell is updated
|
|
360
|
+
as an iframe reference.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
# save the image file on the remote host
|
|
364
|
+
w, h = self._default_size(1920, 1080)
|
|
365
|
+
cmd = f'ensight.render({w},{h},num_samples={self._aa}).save(r"""{self._png_pathname}""")'
|
|
366
|
+
self._session.cmd(cmd)
|
|
367
|
+
# generate HTML page with file references local to the websocket server root
|
|
368
|
+
html = '<body style="margin:0px;padding:0px;">\n'
|
|
369
|
+
html += f'<img src="/{self._png_filename}{self._get_query_parameters_str()}">\n'
|
|
370
|
+
html += "</body>\n"
|
|
371
|
+
# refresh the remote HTML
|
|
372
|
+
self._save_remote_html_page(html)
|
|
373
|
+
super().update()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class RenderableDeepPixel(Renderable):
|
|
377
|
+
"""Renders a deep pixel image on the EnSight host system and makes it available via a webpage."""
|
|
378
|
+
|
|
379
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
380
|
+
super().__init__(*args, **kwargs)
|
|
381
|
+
self._rendertype = "deep_pixel"
|
|
382
|
+
self._generate_url()
|
|
383
|
+
pathname, filename = self._generate_filename(".tif")
|
|
384
|
+
self._tif_pathname = pathname
|
|
385
|
+
self._tif_filename = filename
|
|
386
|
+
# the download is the tiff file
|
|
387
|
+
self._download_names.append(self._tif_filename)
|
|
388
|
+
self.update()
|
|
389
|
+
|
|
390
|
+
def update(self):
|
|
391
|
+
"""Update the deep pixel image and display it.
|
|
392
|
+
|
|
393
|
+
If the renderable is part of a Jupyter notebook cell, that cell is updated as
|
|
394
|
+
an iframe reference.
|
|
395
|
+
"""
|
|
396
|
+
# save the (deep) image file
|
|
397
|
+
# get the optional query parameters which may be an empty string
|
|
398
|
+
# needed for proxy servers like ansys lab
|
|
399
|
+
optional_query = self._get_query_parameters_str()
|
|
400
|
+
w, h = self._default_size(1920, 1080)
|
|
401
|
+
deep = f",num_samples={self._aa},enhanced=1"
|
|
402
|
+
cmd = f'ensight.render({w},{h}{deep}).save(r"""{self._tif_pathname}""")'
|
|
403
|
+
self._session.cmd(cmd)
|
|
404
|
+
html_source = os.path.join(os.path.dirname(__file__), "deep_pixel_view.html")
|
|
405
|
+
with open(html_source, "r") as fp:
|
|
406
|
+
html = fp.read()
|
|
407
|
+
# copy some files from Nexus
|
|
408
|
+
cmd = "import shutil, enve, ceiversion, os.path\n"
|
|
409
|
+
base_name = "os.path.join(enve.home(), f'nexus{ceiversion.nexus_suffix}', 'django', "
|
|
410
|
+
base_name += "'website', 'static', 'website', 'scripts', "
|
|
411
|
+
for script in ["geotiff.js", "geotiff_nexus.js", "bootstrap.min.js"]:
|
|
412
|
+
name = base_name + f"'{script}')"
|
|
413
|
+
cmd += f'shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
|
|
414
|
+
name = "os.path.join(enve.home(), f'nexus{ceiversion.nexus_suffix}', 'django', "
|
|
415
|
+
name += "'website', 'static', 'website', 'content', 'bootstrap.min.css')"
|
|
416
|
+
cmd += f'shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
|
|
417
|
+
self._session.cmd(cmd, do_eval=False)
|
|
418
|
+
# With Bootstrap 5 (2025 R1), class names have '-bs-' in them, e.g. 'data-bs-toggle' vs 'data-toggle'
|
|
419
|
+
bs_prefix = "bs-"
|
|
420
|
+
jquery_version = ""
|
|
421
|
+
if int(self._session._cei_suffix) < 251:
|
|
422
|
+
bs_prefix = ""
|
|
423
|
+
jquery_version = "-3.4.1"
|
|
424
|
+
jquery = f"jquery{jquery_version}.min.js"
|
|
425
|
+
cmd = "import shutil, enve, ceiversion, os.path\n"
|
|
426
|
+
name = base_name + f"'{jquery}')"
|
|
427
|
+
cmd += "try:"
|
|
428
|
+
cmd += f' shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
|
|
429
|
+
cmd += "except Exception:"
|
|
430
|
+
cmd += " pass"
|
|
431
|
+
name = "os.path.join(enve.home(), f'nexus{ceiversion.nexus_suffix}', 'django', "
|
|
432
|
+
name += "'website', 'static', 'website', 'content', 'bootstrap.min.css')"
|
|
433
|
+
cmd += f'shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
|
|
434
|
+
self._session.cmd(cmd, do_eval=False)
|
|
435
|
+
url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
|
|
436
|
+
tiff_url = f"{url}/{self._tif_filename}{optional_query}"
|
|
437
|
+
# replace some bits in the HTML
|
|
438
|
+
html = html.replace("TIFF_URL", tiff_url)
|
|
439
|
+
html = html.replace("ITEMID", self._guid)
|
|
440
|
+
html = html.replace("OPTIONAL_QUERY", optional_query)
|
|
441
|
+
html = html.replace("JQUERY_VERSION", jquery_version)
|
|
442
|
+
html = html.replace("BS_PREFIX", bs_prefix)
|
|
443
|
+
# refresh the remote HTML
|
|
444
|
+
self._save_remote_html_page(html)
|
|
445
|
+
super().update()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class RenderableMP4(Renderable):
|
|
449
|
+
"""Renders the timesteps of the current dataset into an MP4 file and displays the results."""
|
|
450
|
+
|
|
451
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
452
|
+
super().__init__(*args, **kwargs)
|
|
453
|
+
self._rendertype = "animation"
|
|
454
|
+
self._generate_url()
|
|
455
|
+
# the HTML serves up a PNG file
|
|
456
|
+
pathname, filename = self._generate_filename(".mp4")
|
|
457
|
+
self._mp4_pathname = pathname
|
|
458
|
+
self._mp4_filename = filename
|
|
459
|
+
# the download is the mp4 file
|
|
460
|
+
self._download_names.append(self._mp4_filename)
|
|
461
|
+
self.update()
|
|
462
|
+
|
|
463
|
+
def update(self):
|
|
464
|
+
"""Update the animation and display it.
|
|
465
|
+
|
|
466
|
+
If the renderable is part of a Jupyter notebook cell, that cell is updated as an
|
|
467
|
+
iframe reference.
|
|
468
|
+
|
|
469
|
+
"""
|
|
470
|
+
# save the image file on the remote host
|
|
471
|
+
w, h = self._default_size(1920, 1080)
|
|
472
|
+
# Assume this is a particle trace animation save...
|
|
473
|
+
num_frames = self._num_frames
|
|
474
|
+
st = 0
|
|
475
|
+
if self._num_frames is None:
|
|
476
|
+
# get the timestep limits, [0,0] is non-time varying
|
|
477
|
+
st, en = self._session.ensight.objs.core.TIMESTEP_LIMITS
|
|
478
|
+
num_frames = en - st + 1
|
|
479
|
+
self._session.ensight.file.animation_rend_offscreen("ON")
|
|
480
|
+
self._session.ensight.file.animation_screen_tiling(1, 1)
|
|
481
|
+
self._session.ensight.file.animation_format("mpeg4")
|
|
482
|
+
self._session.ensight.file.animation_format_options("Quality High Type 1")
|
|
483
|
+
self._session.ensight.file.animation_frame_rate(self._fps)
|
|
484
|
+
self._session.ensight.file.animation_rend_offscreen("ON")
|
|
485
|
+
self._session.ensight.file.animation_numpasses(self._aa)
|
|
486
|
+
self._session.ensight.file.animation_stereo("mono")
|
|
487
|
+
self._session.ensight.file.animation_screen_tiling(1, 1)
|
|
488
|
+
self._session.ensight.file.animation_file(self._mp4_pathname)
|
|
489
|
+
self._session.ensight.file.animation_window_size("user_defined")
|
|
490
|
+
self._session.ensight.file.animation_window_xy(w, h)
|
|
491
|
+
self._session.ensight.file.animation_frames(num_frames)
|
|
492
|
+
self._session.ensight.file.animation_start_number(st)
|
|
493
|
+
self._session.ensight.file.animation_multiple_images("OFF")
|
|
494
|
+
self._session.ensight.file.animation_raytrace_it("OFF")
|
|
495
|
+
self._session.ensight.file.animation_raytrace_ext("OFF")
|
|
496
|
+
self._session.ensight.file.animation_play_flipbook("OFF")
|
|
497
|
+
self._session.ensight.file.animation_play_keyframe("OFF")
|
|
498
|
+
|
|
499
|
+
if self._num_frames is None:
|
|
500
|
+
# playing over time
|
|
501
|
+
self._session.ensight.file.animation_play_time("ON")
|
|
502
|
+
self._session.ensight.file.animation_reset_traces("OFF")
|
|
503
|
+
self._session.ensight.file.animation_reset_time("ON")
|
|
504
|
+
else:
|
|
505
|
+
# recording particle traces/etc
|
|
506
|
+
self._session.ensight.file.animation_play_time("OFF")
|
|
507
|
+
self._session.ensight.file.animation_reset_traces("ON")
|
|
508
|
+
self._session.ensight.file.animation_reset_time("OFF")
|
|
509
|
+
|
|
510
|
+
self._session.ensight.file.animation_reset_flipbook("OFF")
|
|
511
|
+
self._session.ensight.file.animation_reset_keyframe("OFF")
|
|
512
|
+
self._session.ensight.file.save_animation()
|
|
513
|
+
|
|
514
|
+
# generate HTML page with file references local to the websocket server root
|
|
515
|
+
html = '<body style="margin:0px;padding:0px;">\n'
|
|
516
|
+
html += f'<video width="{w}" height="{h}" controls>\n'
|
|
517
|
+
html += f' <source src="/{self._mp4_filename}{self._get_query_parameters_str()}" type="video/mp4" />\n'
|
|
518
|
+
html += "</video>\n"
|
|
519
|
+
html += "</body>\n"
|
|
520
|
+
|
|
521
|
+
# refresh the remote HTML
|
|
522
|
+
self._save_remote_html_page(html)
|
|
523
|
+
super().update()
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class RenderableWebGL(Renderable):
|
|
527
|
+
"""Renders an AVZ file (WebGL renderable) on the EnSight host system and makes it available via
|
|
528
|
+
a webpage.
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
532
|
+
super().__init__(*args, **kwargs)
|
|
533
|
+
self._rendertype = "webgl"
|
|
534
|
+
self._generate_url()
|
|
535
|
+
pathname, filename = self._generate_filename(".avz")
|
|
536
|
+
self._avz_pathname = pathname
|
|
537
|
+
self._avz_filename = filename
|
|
538
|
+
# the download is the avz file
|
|
539
|
+
self._download_names.append(self._avz_filename)
|
|
540
|
+
self.update()
|
|
541
|
+
|
|
542
|
+
def update(self):
|
|
543
|
+
"""Update the WebGL geometry and display it.
|
|
544
|
+
|
|
545
|
+
If the renderable is part of a Jupyter notebook cell, that cell is updated as
|
|
546
|
+
an iframe reference.
|
|
547
|
+
"""
|
|
548
|
+
# save the .avz file
|
|
549
|
+
self._session.ensight.part.select_all()
|
|
550
|
+
self._session.ensight.savegeom.format("avz")
|
|
551
|
+
# current timestep or all of the timesteps
|
|
552
|
+
ts = self._session.ensight.objs.core.TIMESTEP
|
|
553
|
+
st = ts
|
|
554
|
+
en = ts
|
|
555
|
+
if self._temporal:
|
|
556
|
+
st, en = self._session.ensight.objs.core.TIMESTEP_LIMITS
|
|
557
|
+
self._session.ensight.savegeom.begin_step(st)
|
|
558
|
+
self._session.ensight.savegeom.end_step(en)
|
|
559
|
+
self._session.ensight.savegeom.step_by(1)
|
|
560
|
+
# Save the file
|
|
561
|
+
self._session.ensight.savegeom.save_geometric_entities(self._avz_pathname)
|
|
562
|
+
# generate HTML page with file references local to the websocket server root
|
|
563
|
+
version = _get_ansysnexus_version(self._session._cei_suffix)
|
|
564
|
+
if self._using_proxy:
|
|
565
|
+
# if using pim we get the static content from the front end and not
|
|
566
|
+
# where ensight is running, thus we use a specific URI host and not relative.
|
|
567
|
+
html = f"<script src='{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}/ansys{version}/nexus/viewer-loader.js'></script>\n"
|
|
568
|
+
html += f"<ansys-nexus-viewer src='{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}/{self._avz_filename}"
|
|
569
|
+
html += f"{self._get_query_parameters_str()}'></ansys-nexus-viewer>\n"
|
|
570
|
+
else:
|
|
571
|
+
html = f"<script src='/ansys{version}/nexus/viewer-loader.js'></script>\n"
|
|
572
|
+
html += f"<ansys-nexus-viewer src='/{self._avz_filename}{self._get_query_parameters_str()}'></ansys-nexus-viewer>\n"
|
|
573
|
+
# refresh the remote HTML
|
|
574
|
+
self._save_remote_html_page(html)
|
|
575
|
+
super().update()
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class RenderableVNC(Renderable):
|
|
579
|
+
"""Generates an ansys-nexus-viewer component that can be used to connect to the EnSight VNC remote image renderer."""
|
|
580
|
+
|
|
581
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
582
|
+
ui = kwargs.get("ui")
|
|
583
|
+
if kwargs.get("ui"):
|
|
584
|
+
kwargs.pop("ui")
|
|
585
|
+
super().__init__(*args, **kwargs)
|
|
586
|
+
self._ui = ui
|
|
587
|
+
self._generate_url()
|
|
588
|
+
self._rendertype = "remote"
|
|
589
|
+
self.update()
|
|
590
|
+
|
|
591
|
+
def _update_2023R2_or_less(self):
|
|
592
|
+
"""Update the remote rendering widget and display it for
|
|
593
|
+
backend EnSight of version earlier than 2024R1
|
|
594
|
+
"""
|
|
595
|
+
query_params = {
|
|
596
|
+
"autoconnect": "true",
|
|
597
|
+
"host": self._session.html_hostname,
|
|
598
|
+
"port": self._session.ws_port,
|
|
599
|
+
}
|
|
600
|
+
url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
|
|
601
|
+
url += "/ansys/nexus/novnc/vnc_envision.html"
|
|
602
|
+
url += self._get_query_parameters_str(query_params)
|
|
603
|
+
self._url = url
|
|
604
|
+
|
|
605
|
+
def update(self):
|
|
606
|
+
"""Update the remote rendering widget and display it.
|
|
607
|
+
|
|
608
|
+
If the renderable is part of a Jupyter notebook cell, that cell is updated as an
|
|
609
|
+
iframe reference.
|
|
610
|
+
|
|
611
|
+
"""
|
|
612
|
+
optional_query = self._get_query_parameters_str()
|
|
613
|
+
version = _get_ansysnexus_version(self._session._cei_suffix)
|
|
614
|
+
ui = "simple" if not self._ui else self._ui
|
|
615
|
+
if int(self._session._cei_suffix) < 242: # pragma: no cover
|
|
616
|
+
version = ""
|
|
617
|
+
self._update_2023R2_or_less() # pragma: no cover
|
|
618
|
+
else:
|
|
619
|
+
html = (
|
|
620
|
+
f"<script src='/ansys{version}/nexus/viewer-loader.js{optional_query}'></script>\n"
|
|
621
|
+
)
|
|
622
|
+
rest_uri = (
|
|
623
|
+
f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
|
|
624
|
+
)
|
|
625
|
+
ws_uri = (
|
|
626
|
+
f"{self._http_protocol}://{self._session.html_hostname}:{self._session.ws_port}"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
query_args = ""
|
|
630
|
+
if self._using_proxy and optional_query: # pragma: no cover
|
|
631
|
+
query_args = f', "extra_query_args":"{optional_query[1:]}"' # pragma: no cover
|
|
632
|
+
|
|
633
|
+
attributes = ' renderer="envnc"'
|
|
634
|
+
attributes += f" ui={ui}"
|
|
635
|
+
attributes += ' active="true"'
|
|
636
|
+
attributes += (
|
|
637
|
+
" renderer_options='"
|
|
638
|
+
+ f'{{ "ws":"{ws_uri}", "http":"{rest_uri}", "security_token":"{self._session.secret_key}", "connect_to_running_ens":true {query_args} }}'
|
|
639
|
+
+ "'"
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
html += f"<ansys-nexus-viewer {attributes}></ansys-nexus-viewer>\n"
|
|
643
|
+
|
|
644
|
+
# refresh the remote HTML
|
|
645
|
+
self._save_remote_html_page(html)
|
|
646
|
+
super().update()
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# Undocumented class
|
|
650
|
+
class RenderableVNCAngular(Renderable):
|
|
651
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
652
|
+
super().__init__(*args, **kwargs)
|
|
653
|
+
self._generate_url()
|
|
654
|
+
self._rendertype = "remote"
|
|
655
|
+
self.update()
|
|
656
|
+
|
|
657
|
+
def update(self):
|
|
658
|
+
optional_query = self._get_query_parameters_str()
|
|
659
|
+
version = _get_ansysnexus_version(self._session._cei_suffix)
|
|
660
|
+
base_content = f"""
|
|
661
|
+
<!doctype html>
|
|
662
|
+
<html lang="en" class="dark">
|
|
663
|
+
<head><base href="/ansys{version}/nexus/angular/">
|
|
664
|
+
<meta charset="utf-8">
|
|
665
|
+
<title>WebEnSight</title>
|
|
666
|
+
<script src="/ansys{version}/nexus/viewer-loader.js"></script>
|
|
667
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
668
|
+
<link rel="icon" type="image/x-icon" href="ensight.ico">
|
|
669
|
+
<link rel="stylesheet" href="styles.css"></head>
|
|
670
|
+
<body>
|
|
671
|
+
"""
|
|
672
|
+
module_with_attributes = "\n <web-en-sight "
|
|
673
|
+
module_with_attributes += f'wsPort="{self._session.ws_port}" '
|
|
674
|
+
module_with_attributes += f'secretKey="{self._session.secret_key}"'
|
|
675
|
+
if self._using_proxy and optional_query: # pragma: no cover
|
|
676
|
+
module_with_attributes += f' extraQueryArgs="{optional_query[1:]}"'
|
|
677
|
+
module_with_attributes += ">\n"
|
|
678
|
+
script_src = '<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="main.js" type="module"></script></body>\n</html>'
|
|
679
|
+
content = base_content + module_with_attributes + script_src
|
|
680
|
+
self._save_remote_html_page(content)
|
|
681
|
+
super().update()
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class RenderableEVSN(Renderable):
|
|
685
|
+
"""Generates a URL that can be used to connect to the EnVision VNC remote image renderer."""
|
|
686
|
+
|
|
687
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
688
|
+
super().__init__(*args, **kwargs)
|
|
689
|
+
self._rendertype = "remote_scene"
|
|
690
|
+
self._generate_url()
|
|
691
|
+
pathname, filename = self._generate_filename(".evsn")
|
|
692
|
+
self._evsn_pathname = pathname
|
|
693
|
+
self._evsn_filename = filename
|
|
694
|
+
pathname, filename = self._generate_filename(".png")
|
|
695
|
+
self._proxy_pathname = pathname
|
|
696
|
+
self._proxy_filename = filename
|
|
697
|
+
# the download is the evsn file
|
|
698
|
+
self._download_names.append(self._evsn_filename)
|
|
699
|
+
self.update()
|
|
700
|
+
|
|
701
|
+
def update(self):
|
|
702
|
+
"""Update the remote rendering widget and display it.
|
|
703
|
+
|
|
704
|
+
If the renderable is part of a Jupyter notebook cell, that cell is updated as an
|
|
705
|
+
iframe reference.
|
|
706
|
+
|
|
707
|
+
"""
|
|
708
|
+
# Save the proxy image
|
|
709
|
+
w, h = self._default_size(1920, 1080)
|
|
710
|
+
cmd = f'ensight.render({w},{h},num_samples={self._aa}).save(r"""{self._proxy_pathname}""")'
|
|
711
|
+
self._session.cmd(cmd)
|
|
712
|
+
# save the .evsn file
|
|
713
|
+
self._session.ensight.file.save_scenario_which_parts("all")
|
|
714
|
+
self._session.ensight.file.scenario_format("envision")
|
|
715
|
+
# current timestep or all of the timesteps
|
|
716
|
+
if self._temporal:
|
|
717
|
+
st, en = self._session.ensight.objs.core.TIMESTEP_LIMITS
|
|
718
|
+
self._session.ensight.file.scenario_steptime_anim(1, st, en, 1.0)
|
|
719
|
+
else:
|
|
720
|
+
self._session.ensight.file.scenario_steptime_anim(0, 1, 1, 1)
|
|
721
|
+
varlist = self._session.ensight.objs.core.VARIABLES.find(True, "ACTIVE")
|
|
722
|
+
vars = [x.DESCRIPTION for x in varlist]
|
|
723
|
+
self._session.ensight.variables.select_byname_begin(vars)
|
|
724
|
+
# Save the file
|
|
725
|
+
self._session.ensight.file.save_scenario_fileslct(self._evsn_pathname)
|
|
726
|
+
|
|
727
|
+
# generate HTML page with file references local to the websocketserver root
|
|
728
|
+
optional_query = self._get_query_parameters_str()
|
|
729
|
+
version = _get_ansysnexus_version(self._session._cei_suffix)
|
|
730
|
+
html = f"<script src='/ansys{version}/nexus/viewer-loader.js{optional_query}'></script>\n"
|
|
731
|
+
server = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
|
|
732
|
+
|
|
733
|
+
# FIXME: This method doesn't work with Ansys Lab since the viewer seems to require
|
|
734
|
+
# a full pathname to the file being generated by EnSight on a shared file system.
|
|
735
|
+
# The following commented out line should replace the two after that, but that
|
|
736
|
+
# prevents running locally from working since it's not using the full pathname to
|
|
737
|
+
# the shared file. -MFK
|
|
738
|
+
cleanname = self._evsn_filename.replace("\\", "/")
|
|
739
|
+
attributes = f"src='{cleanname}'"
|
|
740
|
+
# attributes = f"src='{cleanname}{optional_query}'"
|
|
741
|
+
|
|
742
|
+
attributes += f" proxy_img='/{self._proxy_filename}{optional_query}'"
|
|
743
|
+
attributes += " aspect_ratio='proxy'"
|
|
744
|
+
attributes += " renderer='envnc'"
|
|
745
|
+
http_uri = f'"http":"{server}"'
|
|
746
|
+
ws_uri = (
|
|
747
|
+
f'"ws":"{self._http_protocol}://{self._session.html_hostname}:{self._session.ws_port}"'
|
|
748
|
+
)
|
|
749
|
+
secrets = f'"security_token":"{self._session.secret_key}"'
|
|
750
|
+
if not self._using_proxy or not optional_query: # pragma: no cover
|
|
751
|
+
attributes += f" renderer_options='{{ {http_uri}, {ws_uri}, {secrets} }}'"
|
|
752
|
+
elif self._using_proxy and optional_query: # pragma: no cover
|
|
753
|
+
query_args = f'"extra_query_args":"{optional_query[1:]}"' # pragma: no cover
|
|
754
|
+
attributes += f" renderer_options='{{ {http_uri}, {ws_uri}, {secrets}, {query_args} }}'" # pragma: no cover
|
|
755
|
+
html += f"<ansys-nexus-viewer {attributes}></ansys-nexus-viewer>\n"
|
|
756
|
+
# refresh the remote HTML
|
|
757
|
+
self._save_remote_html_page(html)
|
|
758
|
+
super().update()
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class RenderableSGEO(Renderable): # pragma: no cover
|
|
762
|
+
"""Generates a WebGL-based renderable that leverages the SGEO format/viewer interface for progressive geometry transport."""
|
|
763
|
+
|
|
764
|
+
def __init__(self, *args, **kwargs) -> None: # pragma: no cover
|
|
765
|
+
super().__init__(*args, **kwargs)
|
|
766
|
+
self._generate_url()
|
|
767
|
+
pathname, filename = self._generate_filename("")
|
|
768
|
+
# on the server, a JSON block can be accessed via:
|
|
769
|
+
# {_sgeo_base_pathname}/geometry.sgeo
|
|
770
|
+
# and the update files:
|
|
771
|
+
# {_sgeo_base_pathname}/{names}.bin
|
|
772
|
+
self._sgeo_base_pathname = pathname
|
|
773
|
+
self._sgeo_base_filename = filename
|
|
774
|
+
# Create the directory where the sgeo files will go '/{filename}/' URL base
|
|
775
|
+
cmd = f'import os\nos.mkdir(r"""{self._sgeo_base_pathname}""")\n'
|
|
776
|
+
self._session.cmd(cmd, do_eval=False)
|
|
777
|
+
# get a stream ID
|
|
778
|
+
self._stream_id = self._session.ensight.dsg_new_stream(sgeo=1)
|
|
779
|
+
#
|
|
780
|
+
self._revision = 0
|
|
781
|
+
self.update()
|
|
782
|
+
|
|
783
|
+
def update(self): # pragma: no cover
|
|
784
|
+
"""Generate a SGEO geometry file.
|
|
785
|
+
|
|
786
|
+
This method causes the EnSight session to generate an updated geometry SGEO
|
|
787
|
+
file and content and then display the results in any attached WebGL viewer.
|
|
788
|
+
|
|
789
|
+
If the renderable is part of a Jupyter notebook cell, that cell is updated as an
|
|
790
|
+
iframe reference.
|
|
791
|
+
|
|
792
|
+
"""
|
|
793
|
+
# Ask for an update to be generated
|
|
794
|
+
remote_filename = f"{self._sgeo_base_pathname}/geometry.sgeo"
|
|
795
|
+
self._session.ensight.dsg_save_update(
|
|
796
|
+
remote_filename,
|
|
797
|
+
urlprefix=f"/{self._sgeo_base_filename}/",
|
|
798
|
+
stream=self._stream_id,
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
# Update the proxy image
|
|
802
|
+
self._update_proxy()
|
|
803
|
+
|
|
804
|
+
# If the first update, generate the HTML
|
|
805
|
+
if self._revision == 0:
|
|
806
|
+
# generate HTML page with file references local to the websocketserver root
|
|
807
|
+
attributes = (
|
|
808
|
+
f"src='/{self._sgeo_base_filename}/geometry.sgeo{self._get_query_parameters_str()}'"
|
|
809
|
+
)
|
|
810
|
+
attributes += f" proxy_img='/{self._sgeo_base_filename}/proxy.png{self._get_query_parameters_str()}'"
|
|
811
|
+
attributes += " aspect_ratio='proxy'"
|
|
812
|
+
attributes += " renderer='sgeo'"
|
|
813
|
+
version = _get_ansysnexus_version(self._session._cei_suffix)
|
|
814
|
+
html = f"<script src='/ansys{version}/nexus/viewer-loader.js{self._get_query_parameters_str()}'></script>\n"
|
|
815
|
+
html += f"<ansys-nexus-viewer id='{self._guid}' {attributes}></ansys-nexus-viewer>\n"
|
|
816
|
+
html += self._periodic_script()
|
|
817
|
+
# refresh the remote HTML
|
|
818
|
+
self._save_remote_html_page(html)
|
|
819
|
+
# Subsequent updates are handled by the component itself
|
|
820
|
+
super().update()
|
|
821
|
+
|
|
822
|
+
# update the revision file
|
|
823
|
+
rev_filename = f"{self._sgeo_base_pathname}/geometry.rev"
|
|
824
|
+
cmd = f'with open(r"""{rev_filename}""", "w") as fp:\n'
|
|
825
|
+
cmd += f' fp.write("{self._revision}")\n'
|
|
826
|
+
self._session.cmd(cmd, do_eval=False)
|
|
827
|
+
|
|
828
|
+
self._revision += 1
|
|
829
|
+
|
|
830
|
+
def _update_proxy(self):
|
|
831
|
+
"""Replace the current proxy image with the current view."""
|
|
832
|
+
# save a proxy image
|
|
833
|
+
w, h = self._default_size(1920, 1080)
|
|
834
|
+
remote_filename = f"{self._sgeo_base_pathname}/proxy.png"
|
|
835
|
+
cmd = f'ensight.render({w},{h},num_samples={self._aa}).save(r"""{remote_filename}""")'
|
|
836
|
+
self._session.cmd(cmd, do_eval=False)
|
|
837
|
+
|
|
838
|
+
def delete(self) -> None:
|
|
839
|
+
try:
|
|
840
|
+
_ = self._session.ensight.dsg_close_stream(self._stream_id)
|
|
841
|
+
except Exception:
|
|
842
|
+
pass
|
|
843
|
+
super().delete()
|
|
844
|
+
|
|
845
|
+
def _periodic_script(self) -> str:
|
|
846
|
+
html_source = os.path.join(os.path.dirname(__file__), "sgeo_poll.html")
|
|
847
|
+
with open(html_source, "r") as fp:
|
|
848
|
+
html = fp.read()
|
|
849
|
+
revision_uri = f"/{self._sgeo_base_filename}/geometry.rev{self._get_query_parameters_str()}"
|
|
850
|
+
html = html.replace("REVURL_ITEMID", revision_uri)
|
|
851
|
+
html = html.replace("ITEMID", self._guid)
|
|
852
|
+
return html
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
class RenderableFluidsWebUI(Renderable):
|
|
856
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
857
|
+
super().__init__(*args, **kwargs)
|
|
858
|
+
self._session.ensight_version_check("2025 R1")
|
|
859
|
+
warnings.warn("The webUI is still under active development and should be considered beta.")
|
|
860
|
+
self._rendertype = "webui"
|
|
861
|
+
self._generate_url()
|
|
862
|
+
self.update()
|
|
863
|
+
|
|
864
|
+
def _generate_url(self) -> None:
|
|
865
|
+
sha256_hash = hashlib.sha256()
|
|
866
|
+
sha256_hash.update(self._session._secret_key.encode())
|
|
867
|
+
token = sha256_hash.hexdigest()
|
|
868
|
+
optional_query = self._get_query_parameters_str()
|
|
869
|
+
port = self._session._webui_port
|
|
870
|
+
if "instance_name" in self._session._launcher._get_query_parameters():
|
|
871
|
+
instance_name = self._session._launcher._get_query_parameters()["instance_name"]
|
|
872
|
+
# If using PIM, the port needs to be the 443 HTTPS Port;
|
|
873
|
+
port = self._session.html_port
|
|
874
|
+
# In the webUI code there's already a workflow to pass down the query parameter
|
|
875
|
+
# ans_instance_id, just use it
|
|
876
|
+
instance_name = self._session._launcher._get_query_parameters()["instance_name"]
|
|
877
|
+
optional_query = f"?ans_instance_id={instance_name}"
|
|
878
|
+
url = f"{self._http_protocol}://{self._session.html_hostname}:{port}"
|
|
879
|
+
url += f"{optional_query}#{token}"
|
|
880
|
+
self._url = url
|