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

@@ -6,7 +6,7 @@ import subprocess
6
6
  import sys
7
7
  import tempfile
8
8
  from types import ModuleType
9
- from typing import TYPE_CHECKING, Optional, Union
9
+ from typing import TYPE_CHECKING, List, Optional, Union
10
10
  import uuid
11
11
 
12
12
  import psutil
@@ -18,6 +18,199 @@ if TYPE_CHECKING:
18
18
  from ansys.api.pyensight import ensight_api
19
19
 
20
20
 
21
+ class OmniverseKitInstance:
22
+ """Interface to an Omniverse application instance
23
+
24
+ Parameters
25
+ ----------
26
+ pid : int
27
+ The process id of the launched instance
28
+ """
29
+
30
+ def __init__(self, pid: int) -> None:
31
+ self._pid: Optional[int] = pid
32
+ print("CTOR:", pid)
33
+
34
+ def __del__(self) -> None:
35
+ """Close down the instance on delete"""
36
+ self.close()
37
+
38
+ def close(self) -> None:
39
+ """Shutdown the Omniverse instance
40
+
41
+ If the instance associated with this object is still running,
42
+ shut it down.
43
+ """
44
+ if not self.is_running():
45
+ return
46
+ proc = psutil.Process(self._pid)
47
+ for child in proc.children(recursive=True):
48
+ if psutil.pid_exists(child.pid):
49
+ # This can be a race condition, so it is ok if the child is dead already
50
+ try:
51
+ child.kill()
52
+ except psutil.NoSuchProcess:
53
+ pass
54
+ # Same issue, this process might already be shutting down, so NoSuchProcess is ok.
55
+ try:
56
+ proc.kill()
57
+ except psutil.NoSuchProcess:
58
+ pass
59
+ self._pid = None
60
+
61
+ def is_running(self) -> bool:
62
+ """Check if the instance is still running
63
+
64
+ Returns
65
+ -------
66
+ bool
67
+ True if the instance is still running.
68
+ """
69
+ if not self._pid:
70
+ return False
71
+ if psutil.pid_exists(self._pid):
72
+ return True
73
+ self._pid = None
74
+ return False
75
+
76
+
77
+ def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
78
+ """
79
+ Use a combination of the current omniverse application and the information
80
+ in the local .nvidia-omniverse/config/omniverse.toml file to come up with
81
+ the pathname of a kit executable suitable for hosting another copy of the
82
+ ansys.geometry.server kit.
83
+
84
+ Returns
85
+ -------
86
+ Optional[str]
87
+ The pathname of a kit executable or None
88
+
89
+ """
90
+ # parse the toml config file for the location of the installed apps
91
+ try:
92
+ import tomllib
93
+ except ModuleNotFoundError:
94
+ import pip._vendor.tomli as tomllib
95
+
96
+ homedir = os.path.expanduser("~")
97
+ ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
98
+ if not os.path.exists(ov_config):
99
+ return None
100
+ # read the Omniverse configuration toml file
101
+ with open(ov_config, "r") as ov_file:
102
+ ov_data = ov_file.read()
103
+ config = tomllib.loads(ov_data)
104
+ appdir = config.get("paths", {}).get("library_root", fallback_directory)
105
+
106
+ # If we are running inside an Omniverse app, use that information
107
+ try:
108
+ import omni.kit.app
109
+
110
+ # get the current application
111
+ app = omni.kit.app.get_app()
112
+ app_name = app.get_app_filename().split(".")[-1]
113
+ app_version = app.get_app_version().split("-")[0]
114
+ # and where it is installed
115
+ appdir = os.path.join(appdir, f"{app_name}-{app_version}")
116
+ except ModuleNotFoundError:
117
+ # Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
118
+ target = None
119
+ target_version = None
120
+ for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
121
+ test_dir = os.path.dirname(d)
122
+ # the name will be something like "create-2023.2.3"
123
+ name = os.path.basename(test_dir).split("-")
124
+ if len(name) != 2:
125
+ continue
126
+ if name[0] not in ("kit", "create", "view"):
127
+ continue
128
+ if (target_version is None) or (name[1] > target_version):
129
+ target = test_dir
130
+ target_version = name[1]
131
+ if target is None:
132
+ return None
133
+ appdir = target
134
+
135
+ # Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
136
+ # Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
137
+ exe_names = ["kit.sh", "kit"]
138
+ if sys.platform.startswith("win"):
139
+ exe_names = ["kit.bat", "kit.exe"]
140
+
141
+ # look in 4 places...
142
+ for dir_name in [appdir, os.path.join(appdir, "kit")]:
143
+ for exe_name in exe_names:
144
+ if os.path.exists(os.path.join(dir_name, exe_name)):
145
+ return os.path.join(dir_name, exe_name)
146
+
147
+ return None
148
+
149
+
150
+ def launch_kit_instance(
151
+ kit_path: Optional[str] = None,
152
+ extension_paths: Optional[List[str]] = None,
153
+ extensions: Optional[List[str]] = None,
154
+ cli_options: Optional[List[str]] = None,
155
+ log_file: Optional[str] = None,
156
+ log_level: str = "warn",
157
+ ) -> "OmniverseKitInstance":
158
+ """Launch an Omniverse application instance
159
+
160
+ Parameters
161
+ ----------
162
+ kit_path : Optional[str]
163
+ The full pathname of to a binary capable of serving as a kit runner.
164
+ extension_paths : Optional[List[str]]
165
+ List of directory names to include the in search for kits.
166
+ extensions : Optional[List[str]]
167
+ List of kit extensions to be loaded into the launched kit instance.
168
+ log_file : Optional[str]
169
+ The name of a text file where the logging information for the instance will be saved.
170
+ log_level : str
171
+ The level of the logging information to record: "verbose", "info", "warn", "error", "fatal",
172
+ the default is "warn".
173
+
174
+ Returns
175
+ -------
176
+ OmniverseKitInstance
177
+ The object interface for the launched instance
178
+
179
+ Examples
180
+ --------
181
+ Run a simple, empty GUI kit instance.
182
+
183
+ >>> from ansys.pyensight.core.utils import omniverse
184
+ >>> ov = omniverse.launch_kit_instance(extensions=['omni.kit.uiapp'])
185
+
186
+ """
187
+ # build the command line
188
+ if not kit_path:
189
+ kit_path = find_kit_filename()
190
+ if not kit_path:
191
+ raise RuntimeError("Unable to find a suitable Omniverse kit install")
192
+ cmd = [kit_path]
193
+ if extension_paths:
194
+ for path in extension_paths:
195
+ cmd.extend(["--ext-folder", path])
196
+ if extensions:
197
+ for ext in extensions:
198
+ cmd.extend(["--enable", ext])
199
+ if cli_options:
200
+ for opt in cli_options:
201
+ cmd.append(opt)
202
+ if log_level not in ("verbose", "info", "warn", "error", "fatal"):
203
+ raise RuntimeError(f"Invalid logging level: {log_level}")
204
+ cmd.append(f"--/log/level={log_level}")
205
+ if log_file:
206
+ cmd.append(f"--/log/file={log_file}")
207
+ cmd.append("--/log/enabled=true")
208
+ # Launch the process
209
+ env_vars = os.environ.copy()
210
+ p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env_vars)
211
+ return OmniverseKitInstance(p.pid)
212
+
213
+
21
214
  class Omniverse:
22
215
  """Provides the ``ensight.utils.omniverse`` interface.
23
216
 
@@ -56,79 +249,6 @@ class Omniverse:
56
249
  self._interpreter: str = ""
57
250
  self._status_filename: str = ""
58
251
 
59
- @staticmethod
60
- def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
61
- """
62
- Use a combination of the current omniverse application and the information
63
- in the local .nvidia-omniverse/config/omniverse.toml file to come up with
64
- the pathname of a kit executable suitable for hosting another copy of the
65
- ansys.geometry.server kit.
66
-
67
- Returns
68
- -------
69
- Optional[str]
70
- The pathname of a kit executable or None
71
-
72
- """
73
- # parse the toml config file for the location of the installed apps
74
- try:
75
- import tomllib
76
- except ModuleNotFoundError:
77
- import pip._vendor.tomli as tomllib
78
-
79
- homedir = os.path.expanduser("~")
80
- ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
81
- if not os.path.exists(ov_config):
82
- return None
83
- # read the Omniverse configuration toml file
84
- with open(ov_config, "r") as ov_file:
85
- ov_data = ov_file.read()
86
- config = tomllib.loads(ov_data)
87
- appdir = config.get("paths", {}).get("library_root", fallback_directory)
88
-
89
- # If we are running inside an Omniverse app, use that information
90
- try:
91
- import omni.kit.app
92
-
93
- # get the current application
94
- app = omni.kit.app.get_app()
95
- app_name = app.get_app_filename().split(".")[-1]
96
- app_version = app.get_app_version().split("-")[0]
97
- # and where it is installed
98
- appdir = os.path.join(appdir, f"{app_name}-{app_version}")
99
- except ModuleNotFoundError:
100
- # Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
101
- target = None
102
- target_version = None
103
- for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
104
- test_dir = os.path.dirname(d)
105
- # the name will be something like "create-2023.2.3"
106
- name = os.path.basename(test_dir).split("-")
107
- if len(name) != 2:
108
- continue
109
- if name[0] not in ("kit", "create", "view"):
110
- continue
111
- if (target_version is None) or (name[1] > target_version):
112
- target = test_dir
113
- target_version = name[1]
114
- if target is None:
115
- return None
116
- appdir = target
117
-
118
- # Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
119
- # Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
120
- exe_names = ["kit.sh", "kit"]
121
- if sys.platform.startswith("win"):
122
- exe_names = ["kit.bat", "kit.exe"]
123
-
124
- # look in 4 places...
125
- for dir_name in [appdir, os.path.join(appdir, "kit")]:
126
- for exe_name in exe_names:
127
- if os.path.exists(os.path.join(dir_name, exe_name)):
128
- return os.path.join(dir_name, exe_name)
129
-
130
- return None
131
-
132
252
  def _check_modules(self) -> None:
133
253
  """Verify that the Python interpreter is correct
134
254
 
@@ -190,6 +310,7 @@ class Omniverse:
190
310
  live: bool = True,
191
311
  debug_filename: str = "",
192
312
  time_scale: float = 1.0,
313
+ line_width: float = 0.0,
193
314
  options: dict = {},
194
315
  ) -> None:
195
316
  """Ensure that an EnSight dsg -> omniverse server is running
@@ -224,6 +345,9 @@ class Omniverse:
224
345
  If the name of a file is provided, it will be used to save logging information on
225
346
  the connection between EnSight and Omniverse. This option is no longer supported,
226
347
  but the API remains for backwards compatibility.
348
+ line_width : float
349
+ If set, line objects will be represented as "tubes" of the size specified by
350
+ this factor. The default is 0.0 and causes lines not to be exported.
227
351
  options : dict
228
352
  Allows for a fallback for the grpc host/port and the security token.
229
353
  """
@@ -259,6 +383,8 @@ class Omniverse:
259
383
  cmd.extend(["--normalize_geometry", "true"])
260
384
  if time_scale != 1.0:
261
385
  cmd.extend(["--time_scale", str(time_scale)])
386
+ if line_width != 0.0:
387
+ cmd.extend(["--line_width", str(line_width)])
262
388
  if not live:
263
389
  cmd.extend(["--oneshot", "1"])
264
390
  cmd.extend(["--dsg_uri", dsg_uri])
@@ -336,7 +462,7 @@ class Omniverse:
336
462
  self._server_pid = None
337
463
  self._new_status_file(new=False)
338
464
 
339
- def update(self, temporal: bool = False) -> None:
465
+ def update(self, temporal: bool = False, line_width: float = 0.0) -> None:
340
466
  """Update the geometry in Omniverse
341
467
 
342
468
  Export the current EnSight scene to the current Omniverse connection.
@@ -345,10 +471,26 @@ class Omniverse:
345
471
  ----------
346
472
  temporal : bool
347
473
  If True, export all timesteps.
474
+ line_width : float
475
+ If set to a non-zero value, lines will be exported with this thickness.
476
+ This feature is only available in 2025 R2 and later.
348
477
  """
349
478
  update_cmd = "dynamicscenegraph://localhost/client/update"
479
+ prefix = "?"
350
480
  if temporal:
351
- update_cmd += "?timesteps=1"
481
+ update_cmd += f"{prefix}timesteps=1"
482
+ prefix = "&"
483
+ if line_width != 0.0:
484
+ add_linewidth = False
485
+ if isinstance(self._ensight, ModuleType):
486
+ add_linewidth = True
487
+ else:
488
+ # only in 2025 R2 and beyond
489
+ if self._ensight._session.ensight_version_check("2025 R2", exception=False):
490
+ add_linewidth = True
491
+ if add_linewidth:
492
+ update_cmd += f"{prefix}ANSYS_linewidth={line_width}"
493
+ prefix = "&"
352
494
  self._check_modules()
353
495
  if not self.is_running_omniverse():
354
496
  raise RuntimeError("No Omniverse server connection is currently active.")
@@ -78,8 +78,7 @@ class OmniverseGeometryServer(object):
78
78
  normalize_geometry: bool = False,
79
79
  dsg_uri: str = "",
80
80
  monitor_directory: str = "",
81
- line_width: float = -0.0001,
82
- use_lines: bool = False,
81
+ line_width: float = 0.0,
83
82
  ) -> None:
84
83
  self._dsg_uri = dsg_uri
85
84
  self._destination = destination
@@ -96,7 +95,6 @@ class OmniverseGeometryServer(object):
96
95
  self._status_filename: str = ""
97
96
  self._monitor_directory: str = monitor_directory
98
97
  self._line_width = line_width
99
- self._use_lines = use_lines
100
98
 
101
99
  @property
102
100
  def monitor_directory(self) -> Optional[str]:
@@ -173,6 +171,14 @@ class OmniverseGeometryServer(object):
173
171
  def time_scale(self, value: float) -> None:
174
172
  self._time_scale = value
175
173
 
174
+ @property
175
+ def line_width(self) -> float:
176
+ return self._line_width
177
+
178
+ @line_width.setter
179
+ def line_width(self, line_width: float) -> None:
180
+ self._line_width = line_width
181
+
176
182
  def run_server(self, one_shot: bool = False) -> None:
177
183
  """
178
184
  Run a DSG to Omniverse server in process.
@@ -189,11 +195,11 @@ class OmniverseGeometryServer(object):
189
195
 
190
196
  # Build the Omniverse connection
191
197
  omni_link = ov_dsg_server.OmniverseWrapper(
192
- destination=self._destination, line_width=self._line_width, use_lines=self._use_lines
198
+ destination=self._destination, line_width=self.line_width
193
199
  )
194
200
  logging.info("Omniverse connection established.")
195
201
 
196
- # parse the DSG USI
202
+ # parse the DSG URI
197
203
  parsed = urlparse(self.dsg_uri)
198
204
  port = parsed.port
199
205
  host = parsed.hostname
@@ -223,7 +229,10 @@ class OmniverseGeometryServer(object):
223
229
 
224
230
  # until the link is dropped, continue
225
231
  while not dsg_link.is_shutdown() and not self._shutdown:
232
+ # Reset the line width to the CLI default before each update
233
+ omni_link.line_width = self.line_width
226
234
  dsg_link.handle_one_update()
235
+
227
236
  if one_shot:
228
237
  break
229
238
 
@@ -276,7 +285,7 @@ class OmniverseGeometryServer(object):
276
285
 
277
286
  # Build the Omniverse connection
278
287
  omni_link = ov_dsg_server.OmniverseWrapper(
279
- destination=self._destination, line_width=self._line_width, use_lines=self._use_lines
288
+ destination=self._destination, line_width=self.line_width
280
289
  )
281
290
  logging.info("Omniverse connection established.")
282
291
 
@@ -347,6 +356,8 @@ class OmniverseGeometryServer(object):
347
356
  logging.warning("Time values not currently supported.")
348
357
  if len(files_to_process) > 1:
349
358
  logging.warning("Multiple glb files not currently fully supported.")
359
+ # Reset the line width to the CLI default before each update
360
+ omni_link.line_width = self.line_width
350
361
  # Upload the files
351
362
  glb_link.start_uploads([timeline[0], timeline[-1]])
352
363
  for glb_file, timestamp in zip(files_to_process, file_timestamps):
@@ -463,13 +474,12 @@ if __name__ == "__main__":
463
474
  line_default = float(line_default)
464
475
  except ValueError:
465
476
  line_default = None
466
- # Potential future default: -0.0001
467
477
  parser.add_argument(
468
478
  "--line_width",
469
479
  metavar="line_width",
470
480
  default=line_default,
471
481
  type=float,
472
- help=f"Width of lines: >0=absolute size. <0=fraction of diagonal. 0=wireframe. Default: {line_default}",
482
+ help=f"Width of lines: >0=absolute size. <0=fraction of diagonal. 0=none. Default: {line_default}",
473
483
  )
474
484
 
475
485
  # parse the command line
@@ -492,8 +502,7 @@ if __name__ == "__main__":
492
502
  logging.basicConfig(**log_args) # type: ignore
493
503
 
494
504
  # size of lines in data units or fraction of bounding box diagonal
495
- use_lines = args.line_width is not None
496
- line_width = -0.0001
505
+ line_width = 0.0
497
506
  if args.line_width is not None:
498
507
  line_width = args.line_width
499
508
 
@@ -508,7 +517,6 @@ if __name__ == "__main__":
508
517
  vrmode=not args.include_camera,
509
518
  temporal=args.temporal,
510
519
  line_width=line_width,
511
- use_lines=use_lines,
512
520
  )
513
521
 
514
522
  # run the server
@@ -41,8 +41,7 @@ class OmniverseWrapper(object):
41
41
  self,
42
42
  live_edit: bool = False,
43
43
  destination: str = "",
44
- line_width: float = -0.0001,
45
- use_lines: bool = False,
44
+ line_width: float = 0.0,
46
45
  ) -> None:
47
46
  self._cleaned_index = 0
48
47
  self._cleaned_names: dict = {}
@@ -61,7 +60,6 @@ class OmniverseWrapper(object):
61
60
  self.destination = destination
62
61
 
63
62
  self._line_width = line_width
64
- self._use_lines = use_lines
65
63
 
66
64
  @property
67
65
  def destination(self) -> str:
@@ -82,10 +80,6 @@ class OmniverseWrapper(object):
82
80
  def line_width(self, line_width: float) -> None:
83
81
  self._line_width = line_width
84
82
 
85
- @property
86
- def use_lines(self) -> bool:
87
- return self._use_lines
88
-
89
83
  def shutdown(self) -> None:
90
84
  """
91
85
  Shutdown the connection to Omniverse cleanly.
@@ -582,7 +576,8 @@ class OmniverseWrapper(object):
582
576
  pbrShader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(color)
583
577
 
584
578
  material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface")
585
- UsdShade.MaterialBindingAPI(mesh).Bind(material)
579
+ mat_binding_api = UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim())
580
+ mat_binding_api.Bind(material)
586
581
 
587
582
  return material
588
583
 
@@ -639,7 +634,8 @@ class OmniverseWrapper(object):
639
634
  cam = geom_cam.GetCamera()
640
635
  # LOL, not sure why is might be correct, but so far it seems to work???
641
636
  cam.focalLength = camera.fieldofview
642
- cam.clippingRange = Gf.Range1f(0.1, 10)
637
+ dist = (target_pos - cam_pos).GetLength()
638
+ cam.clippingRange = Gf.Range1f(0.1 * dist, 10.0 * dist)
643
639
  look_at = Gf.Matrix4d()
644
640
  look_at.SetLookAt(cam_pos, target_pos, up_vec)
645
641
  trans_row = look_at.GetRow(3)
@@ -723,14 +719,83 @@ class OmniverseUpdateHandler(UpdateHandler):
723
719
  self._group_prims: Dict[int, Any] = dict()
724
720
  self._root_prim = None
725
721
  self._sent_textures = False
722
+ self._updated_camera = False
726
723
 
727
724
  def add_group(self, id: int, view: bool = False) -> None:
728
725
  super().add_group(id, view)
729
726
  group = self.session.groups[id]
727
+
730
728
  if not view:
729
+ # Capture changes in line/sphere sizes if it was not set from cli
730
+ width = self.get_dsg_cmd_attribute(group, "ANSYS_linewidth")
731
+ if width:
732
+ try:
733
+ self._omni.line_width = float(width)
734
+ except ValueError:
735
+ pass
736
+
731
737
  parent_prim = self._group_prims[group.parent_id]
738
+ # get the EnSight object type and the transform matrix
732
739
  obj_type = self.get_dsg_cmd_attribute(group, "ENS_OBJ_TYPE")
733
- matrix = self.group_matrix(group)
740
+ matrix = group.matrix4x4
741
+ # Is this a "case" group (it will contain part of the camera view in the matrix)
742
+ if obj_type == "ENS_CASE":
743
+ if (not self.session.vrmode) and (not self._updated_camera):
744
+ # if in camera mode, we need to update the camera matrix so we can
745
+ # use the identity matrix on this group. The camera should have been
746
+ # created in the "view" handler
747
+ cam_name = "/Root/Cam"
748
+ cam_prim = self._omni._stage.GetPrimAtPath(cam_name) # type: ignore
749
+ geom_cam = UsdGeom.Camera(cam_prim)
750
+ # get the camera
751
+ cam = geom_cam.GetCamera()
752
+ c = cam.transform
753
+ m = Gf.Matrix4d(*matrix).GetTranspose()
754
+ # move the model transform to the camera transform
755
+ cam.transform = c * m.GetInverse()
756
+ # set the updated camera
757
+ geom_cam.SetFromCamera(cam)
758
+ # apply the inverse cam transform to move the center of interest
759
+ # from data space to camera space
760
+ coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
761
+ if coi_attr.IsValid():
762
+ coi_data = coi_attr.Get()
763
+ coi_cam = (
764
+ Gf.Vec4d(coi_data[0], coi_data[1], coi_data[2], 1.0)
765
+ * cam.transform.GetInverse()
766
+ )
767
+ coi_attr.Set(
768
+ Gf.Vec3d(
769
+ coi_cam[0] / coi_cam[3],
770
+ coi_cam[1] / coi_cam[3],
771
+ coi_cam[2] / coi_cam[3],
772
+ )
773
+ )
774
+ # use the camera view by default
775
+ self._omni._stage.GetRootLayer().customLayerData = { # type: ignore
776
+ "cameraSettings": {"boundCamera": "/Root/Cam"}
777
+ }
778
+
779
+ # We only want to do this once
780
+ self._updated_camera = True
781
+ matrix = [
782
+ 1.0,
783
+ 0.0,
784
+ 0.0,
785
+ 0.0,
786
+ 0.0,
787
+ 1.0,
788
+ 0.0,
789
+ 0.0,
790
+ 0.0,
791
+ 0.0,
792
+ 1.0,
793
+ 0.0,
794
+ 0.0,
795
+ 0.0,
796
+ 0.0,
797
+ 1.0,
798
+ ]
734
799
  prim = self._omni.create_dsg_group(
735
800
  group.name, parent_prim, matrix=matrix, obj_type=obj_type
736
801
  )
@@ -801,40 +866,37 @@ class OmniverseUpdateHandler(UpdateHandler):
801
866
  first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
802
867
  mat_info=mat_info,
803
868
  )
804
- if self._omni.use_lines:
805
- command, verts, tcoords, var_cmd = part.line_rep()
806
- if command is not None:
807
- # If there are no triangle (ideally if these are not hidden line
808
- # edges), then use the base color for the part. If there are
809
- # triangles, then assume these are hidden line edges and use the
810
- # line_color.
811
- line_color = color
812
- if has_triangles:
813
- line_color = [
814
- part.cmd.line_color[0] * part.cmd.diffuse,
815
- part.cmd.line_color[1] * part.cmd.diffuse,
816
- part.cmd.line_color[2] * part.cmd.diffuse,
817
- part.cmd.line_color[3],
818
- ]
819
- # TODO: texture coordinates on lines are current invalid in OV
820
- var_cmd = None
821
- tcoords = None
822
- # Generate the lines
823
- _ = self._omni.create_dsg_lines(
824
- name,
825
- obj_id,
826
- part.hash,
827
- parent_prim,
828
- verts,
829
- tcoords,
830
- matrix=matrix,
831
- diffuse=line_color,
832
- variable=var_cmd,
833
- timeline=self.session.cur_timeline,
834
- first_timestep=(
835
- self.session.cur_timeline[0] == self.session.time_limits[0]
836
- ),
837
- )
869
+ command, verts, tcoords, var_cmd = part.line_rep()
870
+ if command is not None:
871
+ # If there are no triangle (ideally if these are not hidden line
872
+ # edges), then use the base color for the part. If there are
873
+ # triangles, then assume these are hidden line edges and use the
874
+ # line_color.
875
+ line_color = color
876
+ if has_triangles:
877
+ line_color = [
878
+ part.cmd.line_color[0] * part.cmd.diffuse,
879
+ part.cmd.line_color[1] * part.cmd.diffuse,
880
+ part.cmd.line_color[2] * part.cmd.diffuse,
881
+ part.cmd.line_color[3],
882
+ ]
883
+ # TODO: texture coordinates on lines are current invalid in OV
884
+ var_cmd = None
885
+ tcoords = None
886
+ # Generate the lines
887
+ _ = self._omni.create_dsg_lines(
888
+ name,
889
+ obj_id,
890
+ part.hash,
891
+ parent_prim,
892
+ verts,
893
+ tcoords,
894
+ matrix=matrix,
895
+ diffuse=line_color,
896
+ variable=var_cmd,
897
+ timeline=self.session.cur_timeline,
898
+ first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
899
+ )
838
900
 
839
901
  elif part.cmd.render == part.cmd.NODES:
840
902
  command, verts, sizes, colors, var_cmd = part.point_rep()
@@ -875,6 +937,8 @@ class OmniverseUpdateHandler(UpdateHandler):
875
937
  # Upload a material to the Omniverse server
876
938
  self._omni.uploadMaterial()
877
939
  self._sent_textures = False
940
+ # We want to update the camera a single time within this update
941
+ self._updated_camera = False
878
942
 
879
943
  def end_update(self) -> None:
880
944
  super().end_update()