ansys-pyensight-core 0.7.9__py3-none-any.whl → 0.7.10__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.

Potentially problematic release.


This version of ansys-pyensight-core might be problematic. Click here for more details.

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