ansys-pyensight-core 0.8.8__py3-none-any.whl → 0.8.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.
- ansys/pyensight/core/__init__.py +1 -1
- ansys/pyensight/core/common.py +216 -0
- ansys/pyensight/core/dockerlauncher.py +85 -94
- ansys/pyensight/core/enshell_grpc.py +32 -0
- ansys/pyensight/core/launch_ensight.py +120 -19
- ansys/pyensight/core/launcher.py +10 -61
- ansys/pyensight/core/libuserd.py +1832 -0
- ansys/pyensight/core/locallauncher.py +47 -2
- ansys/pyensight/core/renderable.py +30 -0
- ansys/pyensight/core/session.py +6 -1
- ansys/pyensight/core/utils/dsg_server.py +227 -35
- ansys/pyensight/core/utils/omniverse.py +84 -24
- ansys/pyensight/core/utils/omniverse_cli.py +481 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +236 -426
- ansys/pyensight/core/utils/omniverse_glb_server.py +279 -0
- ansys/pyensight/core/utils/readers.py +15 -11
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/METADATA +10 -6
- ansys_pyensight_core-0.8.10.dist-info/RECORD +36 -0
- ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/__init__.py +0 -1
- ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/extension.py +0 -407
- ansys/pyensight/core/exts/ansys.geometry.service/config/extension.toml +0 -59
- ansys/pyensight/core/exts/ansys.geometry.service/data/icon.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.service/data/preview.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.service/docs/CHANGELOG.md +0 -11
- ansys/pyensight/core/exts/ansys.geometry.service/docs/README.md +0 -13
- ansys/pyensight/core/exts/ansys.geometry.service/docs/index.rst +0 -18
- ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/__init__.py +0 -1
- ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/extension.py +0 -193
- ansys/pyensight/core/exts/ansys.geometry.serviceui/config/extension.toml +0 -49
- ansys/pyensight/core/exts/ansys.geometry.serviceui/data/icon.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.serviceui/data/preview.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/CHANGELOG.md +0 -11
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/README.md +0 -13
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/index.rst +0 -18
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_BaseColor.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_N.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_ORM.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone.mdl +0 -54
- ansys_pyensight_core-0.8.8.dist-info/RECORD +0 -52
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/LICENSE +0 -0
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/WHEEL +0 -0
|
@@ -12,6 +12,7 @@ import glob
|
|
|
12
12
|
import logging
|
|
13
13
|
import os.path
|
|
14
14
|
import platform
|
|
15
|
+
import re
|
|
15
16
|
import shutil
|
|
16
17
|
import subprocess
|
|
17
18
|
import tempfile
|
|
@@ -20,6 +21,7 @@ from typing import Optional
|
|
|
20
21
|
import uuid
|
|
21
22
|
|
|
22
23
|
import ansys.pyensight.core as pyensight
|
|
24
|
+
from ansys.pyensight.core.common import find_unused_ports
|
|
23
25
|
from ansys.pyensight.core.launcher import Launcher
|
|
24
26
|
import ansys.pyensight.core.session
|
|
25
27
|
|
|
@@ -56,6 +58,8 @@ class LocalLauncher(Launcher):
|
|
|
56
58
|
Number of EnSight servers to use for SOS (Server of Server) mode.
|
|
57
59
|
This parameter is defined on the parent ``Launcher`` class, where
|
|
58
60
|
the default is ``None``, in which case SOS mode is not used.
|
|
61
|
+
additional_command_line_options: list, optional
|
|
62
|
+
Additional command line options to be used to launch EnSight.
|
|
59
63
|
|
|
60
64
|
Examples
|
|
61
65
|
--------
|
|
@@ -87,6 +91,7 @@ class LocalLauncher(Launcher):
|
|
|
87
91
|
# launched process ids
|
|
88
92
|
self._ensight_pid = None
|
|
89
93
|
self._websocketserver_pid = None
|
|
94
|
+
self._webui_pid = None
|
|
90
95
|
# and ports
|
|
91
96
|
self._ports = None
|
|
92
97
|
# Are we running the instance in batch
|
|
@@ -97,6 +102,36 @@ class LocalLauncher(Launcher):
|
|
|
97
102
|
"""Type of app to launch. Options are ``ensight`` and ``envision``."""
|
|
98
103
|
return self._application
|
|
99
104
|
|
|
105
|
+
def launch_webui(self, cpython, webui_script, popen_common):
|
|
106
|
+
cmd = [cpython]
|
|
107
|
+
cmd += [webui_script]
|
|
108
|
+
version = re.findall(r"nexus(\d+)", webui_script)[0]
|
|
109
|
+
path_to_webui = self._install_path
|
|
110
|
+
path_to_webui = os.path.join(
|
|
111
|
+
path_to_webui, f"nexus{version}", f"ansys{version}", "ensight", "WebUI", "web", "ui"
|
|
112
|
+
)
|
|
113
|
+
cmd += ["--server-listen-port", str(self._ports[5])]
|
|
114
|
+
cmd += ["--server-web-roots", path_to_webui]
|
|
115
|
+
cmd += ["--ensight-grpc-port", str(self._ports[0])]
|
|
116
|
+
cmd += ["--ensight-html-port", str(self._ports[2])]
|
|
117
|
+
cmd += ["--ensight-ws-port", str(self._ports[3])]
|
|
118
|
+
cmd += ["--ensight-session-directory", self._session_directory]
|
|
119
|
+
cmd += ["--ensight-secret-key", self._secret_key]
|
|
120
|
+
if "PYENSIGHT_DEBUG" in os.environ:
|
|
121
|
+
try:
|
|
122
|
+
if int(os.environ["PYENSIGHT_DEBUG"]) > 0:
|
|
123
|
+
del popen_common["stdout"]
|
|
124
|
+
del popen_common["stderr"]
|
|
125
|
+
except (ValueError, KeyError):
|
|
126
|
+
pass
|
|
127
|
+
popen_common["env"].update(
|
|
128
|
+
{
|
|
129
|
+
"SIMBA_WEBSERVER_TOKEN": self._secret_key,
|
|
130
|
+
"FLUENT_WEBSERVER_TOKEN": self._secret_key,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
self._webui_pid = subprocess.Popen(cmd, **popen_common).pid
|
|
134
|
+
|
|
100
135
|
def start(self) -> "pyensight.Session":
|
|
101
136
|
"""Start an EnSight session using the local EnSight installation.
|
|
102
137
|
|
|
@@ -124,7 +159,10 @@ class LocalLauncher(Launcher):
|
|
|
124
159
|
|
|
125
160
|
# gRPC port, VNC port, websocketserver ws, websocketserver html
|
|
126
161
|
to_avoid = self._find_ports_used_by_other_pyensight_and_ensight()
|
|
127
|
-
|
|
162
|
+
num_ports = 5
|
|
163
|
+
if self._launch_webui:
|
|
164
|
+
num_ports = 6
|
|
165
|
+
self._ports = find_unused_ports(num_ports, avoid=to_avoid)
|
|
128
166
|
if self._ports is None:
|
|
129
167
|
raise RuntimeError("Unable to allocate local ports for EnSight session")
|
|
130
168
|
is_windows = self._is_windows()
|
|
@@ -154,6 +192,8 @@ class LocalLauncher(Launcher):
|
|
|
154
192
|
vnc_url = f"vnc://%%3Frfb_port={self._ports[1]}%%26use_auth=0"
|
|
155
193
|
cmd.extend(["-vnc", vnc_url])
|
|
156
194
|
cmd.extend(["-ports", str(self._ports[4])])
|
|
195
|
+
if self._additional_command_line_options:
|
|
196
|
+
cmd.extend(self._additional_command_line_options)
|
|
157
197
|
|
|
158
198
|
use_egl = self._use_egl()
|
|
159
199
|
|
|
@@ -207,11 +247,12 @@ class LocalLauncher(Launcher):
|
|
|
207
247
|
except Exception:
|
|
208
248
|
pass
|
|
209
249
|
websocket_script = found_scripts[idx]
|
|
210
|
-
|
|
250
|
+
webui_script = websocket_script.replace("websocketserver.py", "webui_launcher.py")
|
|
211
251
|
# build the commandline
|
|
212
252
|
cmd = [os.path.join(self._install_path, "bin", "cpython"), websocket_script]
|
|
213
253
|
if is_windows:
|
|
214
254
|
cmd[0] += ".bat"
|
|
255
|
+
ensight_python = cmd[0]
|
|
215
256
|
cmd.extend(["--http_directory", self.session_directory])
|
|
216
257
|
# http port
|
|
217
258
|
cmd.extend(["--http_port", str(self._ports[2])])
|
|
@@ -252,9 +293,13 @@ class LocalLauncher(Launcher):
|
|
|
252
293
|
timeout=self._timeout,
|
|
253
294
|
sos=use_sos,
|
|
254
295
|
rest_api=self._enable_rest_api,
|
|
296
|
+
webui_port=self._ports[5] if self._launch_webui else None,
|
|
255
297
|
)
|
|
256
298
|
session.launcher = self
|
|
257
299
|
self._sessions.append(session)
|
|
300
|
+
|
|
301
|
+
if self._launch_webui:
|
|
302
|
+
self.launch_webui(ensight_python, webui_script, popen_common)
|
|
258
303
|
return session
|
|
259
304
|
|
|
260
305
|
def stop(self) -> None:
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
This module provides the interface for creating objects in the EnSight session
|
|
4
4
|
that can be displayed via HTML over the websocket server interface.
|
|
5
5
|
"""
|
|
6
|
+
import hashlib
|
|
6
7
|
import os
|
|
7
8
|
import shutil
|
|
8
9
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, no_type_check
|
|
9
10
|
import uuid
|
|
11
|
+
import warnings
|
|
10
12
|
import webbrowser
|
|
11
13
|
|
|
12
14
|
import requests
|
|
@@ -812,3 +814,31 @@ class RenderableSGEO(Renderable): # pragma: no cover
|
|
|
812
814
|
html = html.replace("REVURL_ITEMID", revision_uri)
|
|
813
815
|
html = html.replace("ITEMID", self._guid)
|
|
814
816
|
return html
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
class RenderableFluidsWebUI(Renderable):
|
|
820
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
821
|
+
super().__init__(*args, **kwargs)
|
|
822
|
+
self._session.ensight_version_check("2025 R1")
|
|
823
|
+
warnings.warn("The webUI is still under active development and should be considered beta.")
|
|
824
|
+
self._rendertype = "webui"
|
|
825
|
+
self._generate_url()
|
|
826
|
+
self.update()
|
|
827
|
+
|
|
828
|
+
def _generate_url(self) -> None:
|
|
829
|
+
sha256_hash = hashlib.sha256()
|
|
830
|
+
sha256_hash.update(self._session._secret_key.encode())
|
|
831
|
+
token = sha256_hash.hexdigest()
|
|
832
|
+
optional_query = self._get_query_parameters_str()
|
|
833
|
+
port = self._session._webui_port
|
|
834
|
+
if "instance_name" in self._session._launcher._get_query_parameters():
|
|
835
|
+
instance_name = self._session._launcher._get_query_parameters()["instance_name"]
|
|
836
|
+
# If using PIM, the port needs to be the 443 HTTPS Port;
|
|
837
|
+
port = self._session.html_port
|
|
838
|
+
# In the webUI code there's already a workflow to pass down the query parameter
|
|
839
|
+
# ans_instance_id, just use it
|
|
840
|
+
instance_name = self._session._launcher._get_query_parameters()["instance_name"]
|
|
841
|
+
optional_query = f"?ans_instance_id={instance_name}"
|
|
842
|
+
url = f"{self._http_protocol}://{self._session.html_hostname}:{port}"
|
|
843
|
+
url += f"{optional_query}#{token}"
|
|
844
|
+
self._url = url
|
ansys/pyensight/core/session.py
CHANGED
|
@@ -31,6 +31,7 @@ from ansys.pyensight.core.listobj import ensobjlist
|
|
|
31
31
|
from ansys.pyensight.core.renderable import (
|
|
32
32
|
RenderableDeepPixel,
|
|
33
33
|
RenderableEVSN,
|
|
34
|
+
RenderableFluidsWebUI,
|
|
34
35
|
RenderableImage,
|
|
35
36
|
RenderableMP4,
|
|
36
37
|
RenderableSGEO,
|
|
@@ -136,6 +137,7 @@ class Session:
|
|
|
136
137
|
timeout: float = 120.0,
|
|
137
138
|
rest_api: bool = False,
|
|
138
139
|
sos: bool = False,
|
|
140
|
+
webui_port: Optional[int] = None,
|
|
139
141
|
) -> None:
|
|
140
142
|
# every session instance needs a unique name that can be used as a cache key
|
|
141
143
|
self._session_name = str(uuid.uuid1())
|
|
@@ -161,6 +163,7 @@ class Session:
|
|
|
161
163
|
self._grpc_port = grpc_port
|
|
162
164
|
self._halt_ensight_on_close = True
|
|
163
165
|
self._callbacks: Dict[str, Tuple[int, Any]] = dict()
|
|
166
|
+
self._webui_port = webui_port
|
|
164
167
|
# if the caller passed a session directory we will assume they are
|
|
165
168
|
# creating effectively a proxy Session and create a (stub) launcher
|
|
166
169
|
if session_directory is not None:
|
|
@@ -927,6 +930,8 @@ class Session:
|
|
|
927
930
|
# Undocumented. Available only internally
|
|
928
931
|
elif what == "webensight":
|
|
929
932
|
render = RenderableVNCAngular(self, **kwargs)
|
|
933
|
+
elif what == "webui":
|
|
934
|
+
render = RenderableFluidsWebUI(self, **kwargs)
|
|
930
935
|
|
|
931
936
|
if render is None:
|
|
932
937
|
raise RuntimeError("Unable to generate requested visualization")
|
|
@@ -1064,7 +1069,7 @@ class Session:
|
|
|
1064
1069
|
onlyfiles = [f for f in listdir(_utils_dir) if os.path.isfile(os.path.join(_utils_dir, f))]
|
|
1065
1070
|
for _basename in onlyfiles:
|
|
1066
1071
|
# skip over any files with the "_server" in their names
|
|
1067
|
-
if "_server" in _basename:
|
|
1072
|
+
if "_server" in _basename or "_cli" in _basename:
|
|
1068
1073
|
continue
|
|
1069
1074
|
_filename = os.path.join(_utils_dir, _basename)
|
|
1070
1075
|
try:
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
1
3
|
import logging
|
|
2
4
|
import os
|
|
3
5
|
import queue
|
|
4
6
|
import sys
|
|
5
7
|
import threading
|
|
8
|
+
import time
|
|
6
9
|
from typing import Any, Dict, List, Optional
|
|
7
10
|
|
|
8
11
|
from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
|
|
@@ -32,9 +35,10 @@ class Part(object):
|
|
|
32
35
|
self.normals = numpy.array([], dtype="float32")
|
|
33
36
|
self.normals_elem = False
|
|
34
37
|
self.tcoords = numpy.array([], dtype="float32")
|
|
35
|
-
self.tcoords_var_id: Optional[int] = None
|
|
36
38
|
self.tcoords_elem = False
|
|
39
|
+
self.node_sizes = numpy.array([], dtype="float32")
|
|
37
40
|
self.cmd: Optional[Any] = None
|
|
41
|
+
self.hash = hashlib.new("sha256")
|
|
38
42
|
self.reset()
|
|
39
43
|
|
|
40
44
|
def reset(self, cmd: Any = None) -> None:
|
|
@@ -46,6 +50,10 @@ class Part(object):
|
|
|
46
50
|
self.tcoords = numpy.array([], dtype="float32")
|
|
47
51
|
self.tcoords_var_id = None
|
|
48
52
|
self.tcoords_elem = False
|
|
53
|
+
self.node_sizes = numpy.array([], dtype="float32")
|
|
54
|
+
self.hash = hashlib.new("sha256")
|
|
55
|
+
if cmd is not None:
|
|
56
|
+
self.hash.update(cmd.hash.encode("utf-8"))
|
|
49
57
|
self.cmd = cmd
|
|
50
58
|
|
|
51
59
|
def update_geom(self, cmd: dynamic_scene_graph_pb2.UpdateGeom) -> None:
|
|
@@ -83,17 +91,25 @@ class Part(object):
|
|
|
83
91
|
):
|
|
84
92
|
# Get the variable definition
|
|
85
93
|
if cmd.variable_id in self.session.variables:
|
|
86
|
-
self.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self.tcoords
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.
|
|
94
|
+
if self.cmd.color_variableid == cmd.variable_id: # type: ignore
|
|
95
|
+
# Receive the colorby var values
|
|
96
|
+
self.tcoords_elem = (
|
|
97
|
+
cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE
|
|
98
|
+
)
|
|
99
|
+
if self.tcoords.size != cmd.total_array_size:
|
|
100
|
+
self.tcoords = numpy.resize(self.tcoords, cmd.total_array_size)
|
|
101
|
+
self.tcoords[
|
|
102
|
+
cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)
|
|
103
|
+
] = cmd.flt_array
|
|
104
|
+
if self.cmd.node_size_variableid == cmd.variable_id: # type: ignore
|
|
105
|
+
# Receive the node size var values
|
|
106
|
+
if self.node_sizes.size != cmd.total_array_size:
|
|
107
|
+
self.node_sizes = numpy.resize(self.node_sizes, cmd.total_array_size)
|
|
108
|
+
self.node_sizes[
|
|
109
|
+
cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)
|
|
110
|
+
] = cmd.flt_array
|
|
111
|
+
# Combine the hashes for the UpdatePart and all UpdateGeom messages
|
|
112
|
+
self.hash.update(cmd.hash.encode("utf-8"))
|
|
97
113
|
|
|
98
114
|
def nodal_surface_rep(self):
|
|
99
115
|
"""
|
|
@@ -120,26 +136,7 @@ class Part(object):
|
|
|
120
136
|
self.session.log(f"Note: part '{self.cmd.name}' contains no triangles.")
|
|
121
137
|
return None, None, None, None, None, None
|
|
122
138
|
verts = self.coords
|
|
123
|
-
|
|
124
|
-
midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
|
|
125
|
-
midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
|
|
126
|
-
midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
|
|
127
|
-
dx = self.session.scene_bounds[3] - self.session.scene_bounds[0]
|
|
128
|
-
dy = self.session.scene_bounds[4] - self.session.scene_bounds[1]
|
|
129
|
-
dz = self.session.scene_bounds[5] - self.session.scene_bounds[2]
|
|
130
|
-
s = dx
|
|
131
|
-
if dy > s:
|
|
132
|
-
s = dy
|
|
133
|
-
if dz > s:
|
|
134
|
-
s = dz
|
|
135
|
-
if s == 0:
|
|
136
|
-
s = 1.0
|
|
137
|
-
num_verts = int(verts.size / 3)
|
|
138
|
-
for i in range(num_verts):
|
|
139
|
-
j = i * 3
|
|
140
|
-
verts[j + 0] = (verts[j + 0] - midx) / s
|
|
141
|
-
verts[j + 1] = (verts[j + 1] - midy) / s
|
|
142
|
-
verts[j + 2] = (verts[j + 2] - midz) / s
|
|
139
|
+
self.normalize_verts(verts)
|
|
143
140
|
|
|
144
141
|
conn = self.conn_tris
|
|
145
142
|
normals = self.normals
|
|
@@ -242,6 +239,141 @@ class Part(object):
|
|
|
242
239
|
|
|
243
240
|
return command, verts, conn, normals, tcoords, var_cmd
|
|
244
241
|
|
|
242
|
+
def normalize_verts(self, verts: numpy.ndarray):
|
|
243
|
+
"""
|
|
244
|
+
This function scales and translates vertices, so the longest axis in the scene is of
|
|
245
|
+
length 1.0, and data is centered at the origin
|
|
246
|
+
|
|
247
|
+
Returns the scale factor
|
|
248
|
+
"""
|
|
249
|
+
s = 1.0
|
|
250
|
+
if self.session.normalize_geometry and self.session.scene_bounds is not None:
|
|
251
|
+
num_verts = int(verts.size / 3)
|
|
252
|
+
midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
|
|
253
|
+
midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
|
|
254
|
+
midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
|
|
255
|
+
dx = self.session.scene_bounds[3] - self.session.scene_bounds[0]
|
|
256
|
+
dy = self.session.scene_bounds[4] - self.session.scene_bounds[1]
|
|
257
|
+
dz = self.session.scene_bounds[5] - self.session.scene_bounds[2]
|
|
258
|
+
s = dx
|
|
259
|
+
if dy > s:
|
|
260
|
+
s = dy
|
|
261
|
+
if dz > s:
|
|
262
|
+
s = dz
|
|
263
|
+
if s == 0:
|
|
264
|
+
s = 1.0
|
|
265
|
+
for i in range(num_verts):
|
|
266
|
+
j = i * 3
|
|
267
|
+
verts[j + 0] = (verts[j + 0] - midx) / s
|
|
268
|
+
verts[j + 1] = (verts[j + 1] - midy) / s
|
|
269
|
+
verts[j + 2] = (verts[j + 2] - midz) / s
|
|
270
|
+
return 1.0 / s
|
|
271
|
+
|
|
272
|
+
def point_rep(self):
|
|
273
|
+
"""
|
|
274
|
+
This function processes the geometry arrays and returns values to represent point data
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
On failure, the method returns None for the first return value. The returned tuple is:
|
|
279
|
+
|
|
280
|
+
(part_command, vertices, sizes, colors, var_command)
|
|
281
|
+
|
|
282
|
+
part_command: UPDATE_PART command object
|
|
283
|
+
vertices: numpy array of per-node coordinates
|
|
284
|
+
sizes: numpy array of per-node radii
|
|
285
|
+
colors: numpy array of per-node rgb colors
|
|
286
|
+
var_command: UPDATE_VARIABLE command object for the variable the colors correspond to, if any
|
|
287
|
+
"""
|
|
288
|
+
if self.cmd is None:
|
|
289
|
+
return None, None, None, None, None
|
|
290
|
+
if self.cmd.render != self.cmd.NODES:
|
|
291
|
+
# Early out. Rendering type for this object is a surface rep, not a point rep
|
|
292
|
+
return None, None, None, None, None
|
|
293
|
+
verts = self.coords
|
|
294
|
+
num_verts = int(verts.size / 3)
|
|
295
|
+
norm_scale = self.normalize_verts(verts)
|
|
296
|
+
|
|
297
|
+
# Convert var values in self.tcoords to RGB colors
|
|
298
|
+
# For now, look up RGB colors. Planned USD enhancements should allow tex coords instead.
|
|
299
|
+
colors = None
|
|
300
|
+
var_cmd = None
|
|
301
|
+
|
|
302
|
+
if self.tcoords.size and self.tcoords.size == num_verts:
|
|
303
|
+
var_dsg_id = self.cmd.color_variableid
|
|
304
|
+
var_cmd = self.session.variables[var_dsg_id]
|
|
305
|
+
if len(var_cmd.levels) == 0:
|
|
306
|
+
self.session.log(
|
|
307
|
+
f"Note: Node rep not created for part '{self.cmd.name}'. It has var values, but a palette with 0 levels."
|
|
308
|
+
)
|
|
309
|
+
return None, None, None, None, None
|
|
310
|
+
|
|
311
|
+
p_min = None
|
|
312
|
+
p_max = None
|
|
313
|
+
for lvl in var_cmd.levels:
|
|
314
|
+
if (p_min is None) or (p_min > lvl.value):
|
|
315
|
+
p_min = lvl.value
|
|
316
|
+
if (p_max is None) or (p_max < lvl.value):
|
|
317
|
+
p_max = lvl.value
|
|
318
|
+
|
|
319
|
+
num_texels = int(len(var_cmd.texture) / 4)
|
|
320
|
+
|
|
321
|
+
colors = numpy.ndarray((num_verts * 3,), dtype="float32")
|
|
322
|
+
low_color = [c / 255.0 for c in var_cmd.texture[0:3]]
|
|
323
|
+
high_color = [
|
|
324
|
+
c / 255.0 for c in var_cmd.texture[4 * (num_texels - 1) : 4 * (num_texels - 1) + 3]
|
|
325
|
+
]
|
|
326
|
+
if p_min == p_max:
|
|
327
|
+
# Special case where palette min == palette max
|
|
328
|
+
mid_color = var_cmd[4 * (num_texels // 2) : 4 * (num_texels // 2) + 3]
|
|
329
|
+
for idx in range(num_verts):
|
|
330
|
+
val = self.tcoords[idx]
|
|
331
|
+
if val == p_min:
|
|
332
|
+
colors[idx * 3 : idx * 3 + 3] = mid_color
|
|
333
|
+
elif val < p_min:
|
|
334
|
+
colors[idx * 3 : idx * 3 + 3] = low_color
|
|
335
|
+
elif val > p_min:
|
|
336
|
+
colors[idx * 3 : idx * 3 + 3] = high_color
|
|
337
|
+
else:
|
|
338
|
+
for idx in range(num_verts):
|
|
339
|
+
val = self.tcoords[idx]
|
|
340
|
+
if val <= p_min:
|
|
341
|
+
colors[idx * 3 : idx * 3 + 3] = low_color
|
|
342
|
+
else:
|
|
343
|
+
pal_pos = (num_texels - 1) * (val - p_min) / (p_max - p_min)
|
|
344
|
+
pal_idx, pal_sub = divmod(pal_pos, 1)
|
|
345
|
+
pal_idx = int(pal_idx)
|
|
346
|
+
|
|
347
|
+
if pal_idx >= num_texels - 1:
|
|
348
|
+
colors[idx * 3 : idx * 3 + 3] = high_color
|
|
349
|
+
else:
|
|
350
|
+
col0 = var_cmd.texture[pal_idx * 4 : pal_idx * 4 + 3]
|
|
351
|
+
col1 = var_cmd.texture[4 + pal_idx * 4 : 4 + pal_idx * 4 + 3]
|
|
352
|
+
for ii in range(0, 3):
|
|
353
|
+
colors[idx * 3 + ii] = (
|
|
354
|
+
col0[ii] * pal_sub + col1[ii] * (1.0 - pal_sub)
|
|
355
|
+
) / 255.0
|
|
356
|
+
self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size/3} points.")
|
|
357
|
+
|
|
358
|
+
node_sizes = None
|
|
359
|
+
if self.node_sizes.size and self.node_sizes.size == num_verts:
|
|
360
|
+
# Pass out the node sizes if there is a size-by variable
|
|
361
|
+
node_size_default = self.cmd.node_size_default * norm_scale
|
|
362
|
+
node_sizes = numpy.ndarray((num_verts,), dtype="float32")
|
|
363
|
+
for ii in range(0, num_verts):
|
|
364
|
+
node_sizes[ii] = self.node_sizes[ii] * node_size_default
|
|
365
|
+
elif norm_scale != 1.0:
|
|
366
|
+
# Pass out the node sizes if the model is normalized to fit in a unit cube
|
|
367
|
+
node_size_default = self.cmd.node_size_default * norm_scale
|
|
368
|
+
node_sizes = numpy.ndarray((num_verts,), dtype="float32")
|
|
369
|
+
for ii in range(0, num_verts):
|
|
370
|
+
node_sizes[ii] = node_size_default
|
|
371
|
+
|
|
372
|
+
self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size/3} points.")
|
|
373
|
+
command = self.cmd
|
|
374
|
+
|
|
375
|
+
return command, verts, node_sizes, colors, var_cmd
|
|
376
|
+
|
|
245
377
|
|
|
246
378
|
class UpdateHandler(object):
|
|
247
379
|
"""
|
|
@@ -410,6 +542,9 @@ class DSGSession(object):
|
|
|
410
542
|
self._scene_bounds: Optional[List] = None
|
|
411
543
|
self._cur_timeline: List = [0.0, 0.0] # Start/End time for current update
|
|
412
544
|
self._callback_handler.session = self
|
|
545
|
+
# log any status changes to this file. external apps will be monitoring
|
|
546
|
+
self._status_file = os.environ.get("ANSYS_OV_SERVER_STATUS_FILENAME", "")
|
|
547
|
+
self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
|
|
413
548
|
|
|
414
549
|
@property
|
|
415
550
|
def scene_bounds(self) -> Optional[List]:
|
|
@@ -474,6 +609,15 @@ class DSGSession(object):
|
|
|
474
609
|
if level < self._verbose:
|
|
475
610
|
logging.info(s)
|
|
476
611
|
|
|
612
|
+
@staticmethod
|
|
613
|
+
def warn(s: str) -> None:
|
|
614
|
+
"""Issue a warning to the logging system
|
|
615
|
+
|
|
616
|
+
The logging message is mapped to "warn" and cannot be blocked via verbosity
|
|
617
|
+
checks.
|
|
618
|
+
"""
|
|
619
|
+
logging.warning(s)
|
|
620
|
+
|
|
477
621
|
def start(self) -> int:
|
|
478
622
|
"""Start a gRPC connection to an EnSight instance
|
|
479
623
|
|
|
@@ -518,6 +662,38 @@ class DSGSession(object):
|
|
|
518
662
|
"""Check the service shutdown request status"""
|
|
519
663
|
return self._shutdown
|
|
520
664
|
|
|
665
|
+
def _update_status_file(self, timed: bool = False):
|
|
666
|
+
"""
|
|
667
|
+
Update the status file contents. The status file will contain the
|
|
668
|
+
following json object, stored as: self._status
|
|
669
|
+
|
|
670
|
+
{
|
|
671
|
+
'status' : "working|idle",
|
|
672
|
+
'start_time' : timestamp_of_update_begin,
|
|
673
|
+
'processed_buffers' : number_of_protobuffers_processed,
|
|
674
|
+
'total_buffers' : number_of_protobuffers_total,
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
Parameters
|
|
678
|
+
----------
|
|
679
|
+
timed : bool, optional:
|
|
680
|
+
if True, only update every second.
|
|
681
|
+
|
|
682
|
+
"""
|
|
683
|
+
if self._status_file:
|
|
684
|
+
current_time = time.time()
|
|
685
|
+
if timed:
|
|
686
|
+
last_time = self._status.get("last_time", 0.0)
|
|
687
|
+
if current_time - last_time < 1.0: # type: ignore
|
|
688
|
+
return
|
|
689
|
+
self._status["last_time"] = current_time
|
|
690
|
+
try:
|
|
691
|
+
message = json.dumps(self._status)
|
|
692
|
+
with open(self._status_file, "w") as status_file:
|
|
693
|
+
status_file.write(message)
|
|
694
|
+
except IOError:
|
|
695
|
+
pass # Note failure is expected here in some cases
|
|
696
|
+
|
|
521
697
|
def request_an_update(self, animation: bool = False, allow_spontaneous: bool = True) -> None:
|
|
522
698
|
"""Start a DSG update
|
|
523
699
|
Send a command to the DSG protocol to "init" an update.
|
|
@@ -590,12 +766,21 @@ class DSGSession(object):
|
|
|
590
766
|
self._mesh_block_count = 0 # reset when a new group shows up
|
|
591
767
|
self._callback_handler.begin_update()
|
|
592
768
|
|
|
769
|
+
# Update our status
|
|
770
|
+
self._status = dict(
|
|
771
|
+
status="working", start_time=time.time(), processed_buffers=1, total_buffers=1
|
|
772
|
+
)
|
|
773
|
+
self._update_status_file()
|
|
774
|
+
|
|
593
775
|
# handle the various commands until UPDATE_SCENE_END
|
|
594
776
|
cmd = self._get_next_message()
|
|
595
777
|
while (cmd is not None) and (
|
|
596
778
|
cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_END
|
|
597
779
|
):
|
|
598
780
|
self._handle_update_command(cmd)
|
|
781
|
+
self._status["processed_buffers"] += 1 # type: ignore
|
|
782
|
+
self._status["total_buffers"] = self._status["processed_buffers"] + self._message_queue.qsize() # type: ignore
|
|
783
|
+
self._update_status_file(timed=True)
|
|
599
784
|
cmd = self._get_next_message()
|
|
600
785
|
|
|
601
786
|
# Flush the last part
|
|
@@ -603,6 +788,10 @@ class DSGSession(object):
|
|
|
603
788
|
|
|
604
789
|
self._callback_handler.end_update()
|
|
605
790
|
|
|
791
|
+
# Update our status
|
|
792
|
+
self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
|
|
793
|
+
self._update_status_file()
|
|
794
|
+
|
|
606
795
|
def _handle_update_command(self, cmd: dynamic_scene_graph_pb2.SceneUpdateCommand) -> None:
|
|
607
796
|
"""Dispatch out a scene update command to the proper handler
|
|
608
797
|
|
|
@@ -647,10 +836,13 @@ class DSGSession(object):
|
|
|
647
836
|
There is always a part being modified. This method completes the current part, committing
|
|
648
837
|
it to the handler.
|
|
649
838
|
"""
|
|
650
|
-
|
|
839
|
+
try:
|
|
840
|
+
self._callback_handler.finalize_part(self.part)
|
|
841
|
+
except Exception as e:
|
|
842
|
+
self.warn(f"Error encountered while finalizing part geometry: {str(e)}")
|
|
651
843
|
self._mesh_block_count += 1
|
|
652
844
|
|
|
653
|
-
def _handle_part(self,
|
|
845
|
+
def _handle_part(self, part_cmd: Any) -> None:
|
|
654
846
|
"""Handle a DSG UPDATE_PART command
|
|
655
847
|
|
|
656
848
|
Finish the current part and set up the next part.
|
|
@@ -661,7 +853,7 @@ class DSGSession(object):
|
|
|
661
853
|
The command coming from the EnSight stream.
|
|
662
854
|
"""
|
|
663
855
|
self._finish_part()
|
|
664
|
-
self._part.reset(
|
|
856
|
+
self._part.reset(part_cmd)
|
|
665
857
|
|
|
666
858
|
def _handle_group(self, group: Any) -> None:
|
|
667
859
|
"""Handle a DSG UPDATE_GROUP command
|