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.

Files changed (41) hide show
  1. ansys/pyensight/core/__init__.py +1 -1
  2. ansys/pyensight/core/common.py +216 -0
  3. ansys/pyensight/core/dockerlauncher.py +85 -94
  4. ansys/pyensight/core/enshell_grpc.py +32 -0
  5. ansys/pyensight/core/launch_ensight.py +120 -19
  6. ansys/pyensight/core/launcher.py +10 -61
  7. ansys/pyensight/core/libuserd.py +1832 -0
  8. ansys/pyensight/core/locallauncher.py +47 -2
  9. ansys/pyensight/core/renderable.py +30 -0
  10. ansys/pyensight/core/session.py +6 -1
  11. ansys/pyensight/core/utils/dsg_server.py +227 -35
  12. ansys/pyensight/core/utils/omniverse.py +84 -24
  13. ansys/pyensight/core/utils/omniverse_cli.py +481 -0
  14. ansys/pyensight/core/utils/omniverse_dsg_server.py +236 -426
  15. ansys/pyensight/core/utils/omniverse_glb_server.py +279 -0
  16. ansys/pyensight/core/utils/readers.py +15 -11
  17. {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/METADATA +10 -6
  18. ansys_pyensight_core-0.8.10.dist-info/RECORD +36 -0
  19. ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/__init__.py +0 -1
  20. ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/extension.py +0 -407
  21. ansys/pyensight/core/exts/ansys.geometry.service/config/extension.toml +0 -59
  22. ansys/pyensight/core/exts/ansys.geometry.service/data/icon.png +0 -0
  23. ansys/pyensight/core/exts/ansys.geometry.service/data/preview.png +0 -0
  24. ansys/pyensight/core/exts/ansys.geometry.service/docs/CHANGELOG.md +0 -11
  25. ansys/pyensight/core/exts/ansys.geometry.service/docs/README.md +0 -13
  26. ansys/pyensight/core/exts/ansys.geometry.service/docs/index.rst +0 -18
  27. ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/__init__.py +0 -1
  28. ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/extension.py +0 -193
  29. ansys/pyensight/core/exts/ansys.geometry.serviceui/config/extension.toml +0 -49
  30. ansys/pyensight/core/exts/ansys.geometry.serviceui/data/icon.png +0 -0
  31. ansys/pyensight/core/exts/ansys.geometry.serviceui/data/preview.png +0 -0
  32. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/CHANGELOG.md +0 -11
  33. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/README.md +0 -13
  34. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/index.rst +0 -18
  35. ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_BaseColor.png +0 -0
  36. ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_N.png +0 -0
  37. ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_ORM.png +0 -0
  38. ansys/pyensight/core/utils/resources/Materials/Fieldstone.mdl +0 -54
  39. ansys_pyensight_core-0.8.8.dist-info/RECORD +0 -52
  40. {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/LICENSE +0 -0
  41. {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/WHEEL +0 -0
@@ -1,9 +1,13 @@
1
1
  import glob
2
+ import json
2
3
  import os
4
+ import platform
3
5
  import subprocess
4
6
  import sys
7
+ import tempfile
5
8
  from types import ModuleType
6
9
  from typing import TYPE_CHECKING, Optional, Union
10
+ import uuid
7
11
 
8
12
  import psutil
9
13
 
@@ -13,8 +17,6 @@ if TYPE_CHECKING:
13
17
  except ImportError:
14
18
  from ansys.api.pyensight import ensight_api
15
19
 
16
- import ansys.pyensight.core
17
-
18
20
 
19
21
  class Omniverse:
20
22
  """Provides the ``ensight.utils.omniverse`` interface.
@@ -52,6 +54,7 @@ class Omniverse:
52
54
  self._ensight = interface
53
55
  self._server_pid: Optional[int] = None
54
56
  self._interpreter: str = ""
57
+ self._status_filename: str = ""
55
58
 
56
59
  @staticmethod
57
60
  def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
@@ -129,10 +132,7 @@ class Omniverse:
129
132
  def _check_modules(self) -> None:
130
133
  """Verify that the Python interpreter is correct
131
134
 
132
- Check for omni module. If not present, raise an exception.
133
- If pxr is there as well, then we can just use sys.executable.
134
- If not, check to see if 'kit.bat' or 'kit.sh' can be found and
135
- arrange to use those instead.
135
+ Check for module dependencies. If not present, raise an exception.
136
136
 
137
137
  Raises
138
138
  ------
@@ -144,11 +144,27 @@ class Omniverse:
144
144
  if len(self._interpreter):
145
145
  return
146
146
 
147
- kit_exe = self.find_kit_filename()
148
- if kit_exe:
149
- self._interpreter = kit_exe
147
+ # if a module, then we are inside EnSight
148
+ if isinstance(self._ensight, ModuleType): # pragma: no cover
149
+ # in this case, we can just use cpython
150
+ import ceiversion
151
+ import enve
152
+
153
+ cei_home = os.environ.get("CEI_HOME", enve.home())
154
+ self._interpreter = os.path.join(cei_home, "bin", f"cpython{ceiversion.apex_suffix}")
155
+ if platform.system() == "Windows":
156
+ self._interpreter += ".bat"
150
157
  return
151
- raise RuntimeError("Unable to detect a copy of the Omniverse kit executable.") from None
158
+ # Using the python interpreter running this code
159
+ self._interpreter = sys.executable
160
+
161
+ # in the future, these will be part of the pyensight wheel
162
+ # dependencies, but for now we include this check.
163
+ try:
164
+ import pxr # noqa: F401
165
+ import pygltflib # noqa: F401
166
+ except Exception:
167
+ raise RuntimeError("Unable to detect omniverse dependencies: usd-core, pygltflib.")
152
168
 
153
169
  def is_running_omniverse(self) -> bool:
154
170
  """Check that an Omniverse connection is active
@@ -228,30 +244,73 @@ class Omniverse:
228
244
  port = options.get("port", 12345)
229
245
  token = options.get("security", "")
230
246
 
231
- # Launch the server via the 'ansys.geometry.service' kit
247
+ # Launch the server via the 'ansys.pyensight.core.utils.omniverse_cli' module
232
248
  dsg_uri = f"grpc://{hostname}:{port}"
233
- kit_dir = os.path.join(os.path.dirname(ansys.pyensight.core.__file__), "exts")
234
249
  cmd = [self._interpreter]
235
- cmd.extend(["--ext-folder", kit_dir])
236
- cmd.extend(["--enable", "ansys.geometry.service"])
250
+ cmd.extend(["-m", "ansys.pyensight.core.utils.omniverse_cli"])
251
+ cmd.append(omniverse_path)
237
252
  if token:
238
- cmd.append(f"--/exts/ansys.geometry.service/securityCode={token}")
253
+ cmd.extend(["--security_token", token])
239
254
  if temporal:
240
- cmd.append("--/exts/ansys.geometry.service/temporal=1")
255
+ cmd.extend(["--temporal", "true"])
241
256
  if not include_camera:
242
- cmd.append("--/exts/ansys.geometry.service/vrmode=1")
257
+ cmd.extend(["--include_camera", "false"])
243
258
  if normalize_geometry:
244
- cmd.append("--/exts/ansys.geometry.service/normalizeGeometry=1")
259
+ cmd.extend(["--normalize_geometry", "true"])
245
260
  if time_scale != 1.0:
246
- cmd.append(f"--/exts/ansys.geometry.service/timeScale={time_scale}")
247
- cmd.append(f"--/exts/ansys.geometry.service/omniUrl={omniverse_path}")
248
- cmd.append(f"--/exts/ansys.geometry.service/dsgUrl={dsg_uri}")
249
- cmd.append("--/exts/ansys.geometry.service/run=1")
261
+ cmd.extend(["--time_scale", str(time_scale)])
262
+ if not live:
263
+ cmd.extend(["--oneshot", "1"])
264
+ cmd.extend(["--dsg_uri", dsg_uri])
250
265
  env_vars = os.environ.copy()
251
- working_dir = os.path.join(os.path.dirname(ansys.pyensight.core.__file__), "utils")
252
- process = subprocess.Popen(cmd, close_fds=True, env=env_vars, cwd=working_dir)
266
+ # we are launching the kit from EnSight or PyEnSight. In these cases, we
267
+ # inform the kit instance of:
268
+ # (1) the name of the "server status" file, if any
269
+ self._new_status_file()
270
+ env_vars["ANSYS_OV_SERVER_STATUS_FILENAME"] = self._status_filename
271
+ process = subprocess.Popen(cmd, close_fds=True, env=env_vars)
253
272
  self._server_pid = process.pid
254
273
 
274
+ def _new_status_file(self, new=True) -> None:
275
+ """
276
+ Remove any existing status file and create a new one if requested.
277
+
278
+ Parameters
279
+ ----------
280
+ new : bool
281
+ If True, create a new status file.
282
+ """
283
+ if self._status_filename:
284
+ try:
285
+ os.remove(self._status_filename)
286
+ except OSError:
287
+ pass
288
+ self._status_filename = ""
289
+ if new:
290
+ self._status_filename = os.path.join(
291
+ tempfile.gettempdir(), str(uuid.uuid1()) + "_gs_status.txt"
292
+ )
293
+
294
+ def read_status_file(self) -> dict:
295
+ """Read the status file and return its contents as a dictionary.
296
+
297
+ Note: this can fail if the file is being written to when this call is made, so expect
298
+ failures.
299
+
300
+ Returns
301
+ -------
302
+ Optional[dict]
303
+ A dictionary with the fields 'status', 'start_time', 'processed_buffers', 'total_buffers' or empty
304
+ """
305
+ if not self._status_filename:
306
+ return {}
307
+ try:
308
+ with open(self._status_filename, "r") as status_file:
309
+ data = json.load(status_file)
310
+ except Exception:
311
+ return {}
312
+ return data
313
+
255
314
  def close_connection(self) -> None:
256
315
  """Shut down the open EnSight dsg -> omniverse server
257
316
 
@@ -275,6 +334,7 @@ class Omniverse:
275
334
  except psutil.NoSuchProcess:
276
335
  pass
277
336
  self._server_pid = None
337
+ self._new_status_file(new=False)
278
338
 
279
339
  def update(self, temporal: bool = False) -> None:
280
340
  """Update the geometry in Omniverse
@@ -0,0 +1,481 @@
1
+ import argparse
2
+ from functools import partial
3
+ import glob
4
+ import json
5
+ import logging
6
+ import os
7
+ import pathlib
8
+ import time
9
+ from typing import Any, List, Optional
10
+ from urllib.parse import urlparse
11
+
12
+ import ansys.pyensight.core
13
+ import ansys.pyensight.core.utils.dsg_server as dsg_server
14
+ import ansys.pyensight.core.utils.omniverse_dsg_server as ov_dsg_server
15
+ import ansys.pyensight.core.utils.omniverse_glb_server as ov_glb_server
16
+
17
+
18
+ def str2bool_type(v: Any) -> bool:
19
+ """
20
+ This function is designed to be a 'type=' filter for an argparse entry returning a boolean.
21
+ It allows for additional, common alternative strings as booleans. These include 'yes','no',
22
+ 'true','false','t','f','y','n','1' and '0'. If the value does not meet the requirements,
23
+ the function will raise the argparse.ArgumentTypeError exception.
24
+ :param v: The (potential) boolean argument.
25
+ :return: The actual boolean value.
26
+ :raises: argparse.ArgumentTypeError
27
+ """
28
+ if isinstance(v, bool):
29
+ return v
30
+ if v.lower() in ("yes", "true", "t", "y", "1"):
31
+ return True
32
+ elif v.lower() in ("no", "false", "f", "n", "0"):
33
+ return False
34
+ else:
35
+ raise argparse.ArgumentTypeError("Boolean value expected.")
36
+
37
+
38
+ def int_range_type(
39
+ v: Any, min_value: int = 0, max_value: int = 100, allow: Optional[List[int]] = None
40
+ ) -> int:
41
+ """
42
+ This function is designed to be a 'type=' filter for an argparse entry returning an integer value within
43
+ a specified range. If the value does not meet the requirements, the function will raise the
44
+ argparse.ArgumentTypeError exception. This function is normally used with functools.partial to bind
45
+ the minimum and maximum values. For example: type=partial(int_range_type, min_value=0, max_value=65535)
46
+ :param v: The (potential) integer argument.
47
+ :param min_value: The minimum legal integer value.
48
+ :param max_value: The maximum legal integer value.
49
+ :param allow:A list of additional, legal values
50
+ :return: The validated integer value.
51
+ :raises: argparse.ArgumentTypeError
52
+ """
53
+ try:
54
+ value = int(v)
55
+ except ValueError:
56
+ raise argparse.ArgumentTypeError("Integer value expected.")
57
+ if allow is None:
58
+ allow = []
59
+ if (value >= min_value) and (value <= max_value):
60
+ return value
61
+ elif value in allow:
62
+ return value
63
+ else:
64
+ msg = f"Integer value is not in the range [{min_value},{max_value}]"
65
+ if allow:
66
+ msg += f" or in the list {allow}"
67
+ raise argparse.ArgumentTypeError(msg + ".")
68
+
69
+
70
+ class OmniverseGeometryServer(object):
71
+ def __init__(
72
+ self,
73
+ security_token: str = "",
74
+ destination: str = "",
75
+ temporal: bool = False,
76
+ vrmode: bool = False,
77
+ time_scale: float = 1.0,
78
+ normalize_geometry: bool = False,
79
+ dsg_uri: str = "",
80
+ monitor_directory: str = "",
81
+ ) -> None:
82
+ self._dsg_uri = dsg_uri
83
+ self._destination = destination
84
+ self._security_token = security_token
85
+ if not self._security_token:
86
+ self._security_token = os.environ.get("ENSIGHT_SECURITY_TOKEN", "")
87
+ self._temporal = temporal
88
+ self._vrmode = vrmode
89
+ self._time_scale = time_scale
90
+ self._normalize_geometry = normalize_geometry
91
+ self._version = "unknown"
92
+ self._shutdown = False
93
+ self._server_process = None
94
+ self._status_filename: str = ""
95
+ self._monitor_directory: str = monitor_directory
96
+
97
+ @property
98
+ def monitor_directory(self) -> Optional[str]:
99
+ if self._monitor_directory:
100
+ return self._monitor_directory
101
+ # converts "" -> None
102
+ return None
103
+
104
+ @property
105
+ def pyensight_version(self) -> str:
106
+ """The ansys.pyensight.core version"""
107
+ return ansys.pyensight.core.VERSION
108
+
109
+ @property
110
+ def dsg_uri(self) -> str:
111
+ """The endpoint of a Dynamic Scene Graph service: grpc://{hostname}:{port}"""
112
+ return self._dsg_uri
113
+
114
+ @dsg_uri.setter
115
+ def dsg_uri(self, uri: str) -> None:
116
+ self._dsg_uri = uri
117
+
118
+ @property
119
+ def destination(self) -> str:
120
+ """The endpoint of an Omniverse Nucleus service: omniverse://{hostname}/{path}"""
121
+ return self._destination
122
+
123
+ @destination.setter
124
+ def destination(self, value: str) -> None:
125
+ self._destination = value
126
+
127
+ @property
128
+ def security_token(self) -> str:
129
+ """The security token of the DSG service instance."""
130
+ return self._security_token
131
+
132
+ @security_token.setter
133
+ def security_token(self, value: str) -> None:
134
+ self._security_token = value
135
+
136
+ @property
137
+ def temporal(self) -> bool:
138
+ """If True, the DSG update should include all timesteps."""
139
+ return self._temporal
140
+
141
+ @temporal.setter
142
+ def temporal(self, value: bool) -> None:
143
+ self._temporal = bool(value)
144
+
145
+ @property
146
+ def vrmode(self) -> bool:
147
+ """If True, the DSG update should not include camera transforms."""
148
+ return self._vrmode
149
+
150
+ @vrmode.setter
151
+ def vrmode(self, value: bool) -> None:
152
+ self._vrmode = bool(value)
153
+
154
+ @property
155
+ def normalize_geometry(self) -> bool:
156
+ """If True, the DSG geometry should be remapped into normalized space."""
157
+ return self._normalize_geometry
158
+
159
+ @normalize_geometry.setter
160
+ def normalize_geometry(self, val: bool) -> None:
161
+ self._normalize_geometry = val
162
+
163
+ @property
164
+ def time_scale(self) -> float:
165
+ """Value to multiply DSG time values by before passing to Omniverse"""
166
+ return self._time_scale
167
+
168
+ @time_scale.setter
169
+ def time_scale(self, value: float) -> None:
170
+ self._time_scale = value
171
+
172
+ def run_server(self, one_shot: bool = False) -> None:
173
+ """
174
+ Run a DSG to Omniverse server in process.
175
+
176
+ Note: this method does not return until the DSG connection is dropped or
177
+ self.stop_server() has been called.
178
+
179
+ Parameters
180
+ ----------
181
+ one_shot : bool
182
+ If True, only run the server to transfer a single scene and
183
+ then return.
184
+ """
185
+
186
+ # Build the Omniverse connection
187
+ omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
188
+ logging.info("Omniverse connection established.")
189
+
190
+ # parse the DSG USI
191
+ parsed = urlparse(self.dsg_uri)
192
+ port = parsed.port
193
+ host = parsed.hostname
194
+
195
+ # link it to a DSG session
196
+ update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
197
+ dsg_link = dsg_server.DSGSession(
198
+ port=port,
199
+ host=host,
200
+ vrmode=self.vrmode,
201
+ security_code=self.security_token,
202
+ verbose=1,
203
+ normalize_geometry=self.normalize_geometry,
204
+ time_scale=self.time_scale,
205
+ handler=update_handler,
206
+ )
207
+
208
+ # Start the DSG link
209
+ logging.info(f"Making DSG connection to: {self.dsg_uri}")
210
+ err = dsg_link.start()
211
+ if err < 0:
212
+ logging.error("Omniverse connection failed.")
213
+ return
214
+
215
+ # Initial pull request
216
+ dsg_link.request_an_update(animation=self.temporal)
217
+
218
+ # until the link is dropped, continue
219
+ while not dsg_link.is_shutdown() and not self._shutdown:
220
+ dsg_link.handle_one_update()
221
+ if one_shot:
222
+ break
223
+
224
+ logging.info("Shutting down DSG connection")
225
+ dsg_link.end()
226
+ omni_link.shutdown()
227
+
228
+ def run_monitor(self):
229
+ """
230
+ Run monitor and upload GLB files to Omniverse in process. There are two cases:
231
+
232
+ 1) the "directory name" is actually a .glb file. In this case, simply push
233
+ the glb file contents to Omniverse.
234
+
235
+ 2) If a directory, then we periodically scan the directory for files named "*.upload".
236
+ If this file is found, there are two cases:
237
+
238
+ a) The file is empty. In this case, for a file named ABC.upload, the file
239
+ ABC.glb will be read and uploaded before both files are deleted.
240
+
241
+ b) The file contains valid json. In this case, the json object is parsed with
242
+ the following format (two glb files for the first timestep and one for the second):
243
+
244
+ {
245
+ "version": 1,
246
+ "destination": "",
247
+ "files": ["a.glb", "b.glb", "c.glb"],
248
+ "times": [0.0, 0.0, 1.0]
249
+ }
250
+
251
+ "times" is optional and defaults to [0*len("files")]. Once processed,
252
+ all the files referenced in the json and the json file itself are deleted.
253
+ "omniuri" is optional and defaults to the passed Omniverse path.
254
+
255
+ Note: In this mode, the method does not return until a "shutdown" file or
256
+ an error is encountered.
257
+
258
+ TODO: add "push" mechanism to trigger a DSG push from the connected session. This
259
+ can be done via the monitor mechanism and used by the Omniverse kit to implement
260
+ a "pull".
261
+ """
262
+ the_dir = self.monitor_directory
263
+ single_file_upload = False
264
+ if os.path.isfile(the_dir) and the_dir.lower().endswith(".glb"):
265
+ single_file_upload = True
266
+ else:
267
+ if not os.path.isdir(the_dir):
268
+ logging.error(f"The monitor directory {the_dir} does not exist.")
269
+ return
270
+
271
+ # Build the Omniverse connection
272
+ omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
273
+ logging.info("Omniverse connection established.")
274
+
275
+ # use an OmniverseUpdateHandler
276
+ update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
277
+
278
+ # Link it to the GLB file monitoring service
279
+ glb_link = ov_glb_server.GLBSession(
280
+ verbose=1,
281
+ handler=update_handler,
282
+ )
283
+ if single_file_upload:
284
+ start_time = time.time()
285
+ logging.info(f"Uploading file: {the_dir}.")
286
+ try:
287
+ glb_link.upload_file(the_dir)
288
+ except Exception as error:
289
+ logging.warning(f"Error uploading file: {the_dir}: {error}")
290
+ logging.info(f"Uploaded in {(time.time() - start_time):.2f}")
291
+ else:
292
+ logging.info(f"Starting file monitoring for {the_dir}.")
293
+ the_dir_path = pathlib.Path(the_dir)
294
+ try:
295
+ stop_file = os.path.join(the_dir, "shutdown")
296
+ orig_destination = omni_link.destination
297
+ while not os.path.exists(stop_file):
298
+ loop_time = time.time()
299
+ files_to_remove = []
300
+ for filename in glob.glob(os.path.join(the_dir, "*", "*.upload")):
301
+ # reset to the launch URI/directory
302
+ omni_link.destination = orig_destination
303
+ # Keep track of the files and time values
304
+ files_to_remove.append(filename)
305
+ files_to_process = []
306
+ file_timestamps = []
307
+ if os.path.getsize(filename) == 0:
308
+ # replace the ".upload" extension with ".glb"
309
+ glb_file = os.path.splitext(filename)[0] + ".glb"
310
+ if os.path.exists(glb_file):
311
+ files_to_process.append(glb_file)
312
+ file_timestamps.append(0.0)
313
+ files_to_remove.append(glb_file)
314
+ else:
315
+ # read the .upload file json content
316
+ try:
317
+ with open(filename, "r") as fp:
318
+ glb_info = json.load(fp)
319
+ except Exception:
320
+ logging.warning(f"Error reading file: {filename}")
321
+ continue
322
+ # if specified, set the URI/directory target
323
+ omni_link.destination = glb_info.get("destination", orig_destination)
324
+ # Get the GLB files to process
325
+ the_files = glb_info.get("files", [])
326
+ files_to_remove.extend(the_files)
327
+ # Times not used for now, but parse them anyway
328
+ the_times = glb_info.get("times", [0.0] * len(the_files))
329
+ # Validate a few things
330
+ if len(the_files) != len(the_times):
331
+ logging.warning(
332
+ f"Number of times and files are not the same in: {filename}"
333
+ )
334
+ continue
335
+ if len(set(the_times)) != 1:
336
+ logging.warning("Time values not currently supported.")
337
+ if len(the_files) > 1:
338
+ logging.warning("Multiple glb files not currently fully supported.")
339
+ files_to_process.extend(the_files)
340
+ # Upload the files
341
+ for glb_file in files_to_process:
342
+ start_time = time.time()
343
+ logging.info(
344
+ f"Uploading file: {glb_file} to {omni_link.destination}."
345
+ )
346
+ try:
347
+ glb_link.upload_file(glb_file)
348
+ except Exception as error:
349
+ logging.warning(f"Error uploading file: {glb_file}: {error}")
350
+ logging.info(f"Uploaded in {(time.time() - start_time):%.2f}")
351
+ for filename in files_to_remove:
352
+ try:
353
+ # Only delete the file if it is in the_dir_path
354
+ filename_path = pathlib.Path(filename)
355
+ if filename_path.is_relative_to(the_dir_path):
356
+ os.remove(filename)
357
+ except IOError:
358
+ pass
359
+ if time.time() - loop_time < 0.1:
360
+ time.sleep(0.25)
361
+ except Exception as error:
362
+ logging.error(f"Error encountered while monitoring: {error}")
363
+ logging.info("Stopping file monitoring.")
364
+ os.remove(stop_file)
365
+
366
+ omni_link.shutdown()
367
+
368
+
369
+ if __name__ == "__main__":
370
+ parser = argparse.ArgumentParser(description="PyEnSight Omniverse Geometry Service")
371
+ parser.add_argument(
372
+ "destination", default="", type=str, help="The directory to save the USD scene graph into."
373
+ )
374
+ parser.add_argument(
375
+ "--verbose",
376
+ metavar="verbose_level",
377
+ default=0,
378
+ type=partial(int_range_type, min_value=0, max_value=3),
379
+ help="Enable logging information (0-3). Default: 0",
380
+ )
381
+ parser.add_argument(
382
+ "--log_file",
383
+ metavar="log_filename",
384
+ default="",
385
+ type=str,
386
+ help="Save logging output to the named log file instead of stdout.",
387
+ )
388
+ parser.add_argument(
389
+ "--dsg_uri",
390
+ default="grpc://127.0.0.1:5234",
391
+ type=str,
392
+ help="The URI of the EnSight Dynamic Scene Graph server. Default: grpc://127.0.0.1:5234",
393
+ )
394
+ parser.add_argument(
395
+ "--security_token",
396
+ metavar="token",
397
+ default="",
398
+ type=str,
399
+ help="Dynamic scene graph API security token. Default: none",
400
+ )
401
+ parser.add_argument(
402
+ "--monitor_directory",
403
+ metavar="glb_directory",
404
+ default="",
405
+ type=str,
406
+ help="Monitor specified directory for GLB files to be exported. Default: none",
407
+ )
408
+ parser.add_argument(
409
+ "--time_scale",
410
+ metavar="time_scale",
411
+ default=1.0,
412
+ type=float,
413
+ help="Scaling factor to be applied to input time values. Default: 1.0",
414
+ )
415
+ parser.add_argument(
416
+ "--normalize_geometry",
417
+ metavar="yes|no|true|false|1|0",
418
+ default=False,
419
+ type=str2bool_type,
420
+ help="Enable mapping of geometry to a normalized Cartesian space. Default: false",
421
+ )
422
+ parser.add_argument(
423
+ "--include_camera",
424
+ metavar="yes|no|true|false|1|0",
425
+ default=True,
426
+ type=str2bool_type,
427
+ help="Include the camera in the output USD scene graph. Default: true",
428
+ )
429
+ parser.add_argument(
430
+ "--temporal",
431
+ metavar="yes|no|true|false|1|0",
432
+ default=False,
433
+ type=str2bool_type,
434
+ help="Export a temporal scene graph. Default: false",
435
+ )
436
+ parser.add_argument(
437
+ "--oneshot",
438
+ metavar="yes|no|true|false|1|0",
439
+ default=False,
440
+ type=str2bool_type,
441
+ help="Convert a single geometry into USD and exit. Default: false",
442
+ )
443
+
444
+ # parse the command line
445
+ args = parser.parse_args()
446
+
447
+ # set up logging
448
+ level = logging.ERROR
449
+ if args.verbose == 1:
450
+ level = logging.WARN
451
+ elif args.verbose == 2:
452
+ level = logging.INFO
453
+ elif args.verbose == 3:
454
+ level = logging.DEBUG
455
+ log_args = dict(format="GeometryService:%(levelname)s:%(message)s", level=level)
456
+ if args.log_file:
457
+ log_args["filename"] = args.log_file
458
+ # start with a clean logging instance
459
+ while logging.root.hasHandlers():
460
+ logging.root.removeHandler(logging.root.handlers[0])
461
+ logging.basicConfig(**log_args) # type: ignore
462
+
463
+ # Build the server object
464
+ server = OmniverseGeometryServer(
465
+ destination=args.destination,
466
+ dsg_uri=args.dsg_uri,
467
+ security_token=args.security_token,
468
+ monitor_directory=args.monitor_directory,
469
+ time_scale=args.time_scale,
470
+ normalize_geometry=args.normalize_geometry,
471
+ vrmode=not args.include_camera,
472
+ temporal=args.temporal,
473
+ )
474
+
475
+ # run the server
476
+ logging.info("Server startup.")
477
+ if server.monitor_directory:
478
+ server.run_monitor()
479
+ else:
480
+ server.run_server(one_shot=args.oneshot)
481
+ logging.info("Server shutdown.")