ansys-pyensight-core 0.9.1__py3-none-any.whl → 0.9.3__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,198 @@ 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
+
33
+ def __del__(self) -> None:
34
+ """Close down the instance on delete"""
35
+ self.close()
36
+
37
+ def close(self) -> None:
38
+ """Shutdown the Omniverse instance
39
+
40
+ If the instance associated with this object is still running,
41
+ shut it down.
42
+ """
43
+ if not self.is_running():
44
+ return
45
+ proc = psutil.Process(self._pid)
46
+ for child in proc.children(recursive=True):
47
+ if psutil.pid_exists(child.pid):
48
+ # This can be a race condition, so it is ok if the child is dead already
49
+ try:
50
+ child.kill()
51
+ except psutil.NoSuchProcess:
52
+ pass
53
+ # Same issue, this process might already be shutting down, so NoSuchProcess is ok.
54
+ try:
55
+ proc.kill()
56
+ except psutil.NoSuchProcess:
57
+ pass
58
+ self._pid = None
59
+
60
+ def is_running(self) -> bool:
61
+ """Check if the instance is still running
62
+
63
+ Returns
64
+ -------
65
+ bool
66
+ True if the instance is still running.
67
+ """
68
+ if not self._pid:
69
+ return False
70
+ if psutil.pid_exists(self._pid):
71
+ return True
72
+ self._pid = None
73
+ return False
74
+
75
+
76
+ def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
77
+ """
78
+ Use a combination of the current omniverse application and the information
79
+ in the local .nvidia-omniverse/config/omniverse.toml file to come up with
80
+ the pathname of a kit executable suitable for hosting another copy of the
81
+ ansys.geometry.server kit.
82
+
83
+ Returns
84
+ -------
85
+ Optional[str]
86
+ The pathname of a kit executable or None
87
+
88
+ """
89
+ # parse the toml config file for the location of the installed apps
90
+ try:
91
+ import tomllib
92
+ except ModuleNotFoundError:
93
+ import pip._vendor.tomli as tomllib
94
+
95
+ homedir = os.path.expanduser("~")
96
+ ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
97
+ if not os.path.exists(ov_config):
98
+ return None
99
+ # read the Omniverse configuration toml file
100
+ with open(ov_config, "r") as ov_file:
101
+ ov_data = ov_file.read()
102
+ config = tomllib.loads(ov_data)
103
+ appdir = config.get("paths", {}).get("library_root", fallback_directory)
104
+
105
+ # If we are running inside an Omniverse app, use that information
106
+ try:
107
+ import omni.kit.app
108
+
109
+ # get the current application
110
+ app = omni.kit.app.get_app()
111
+ app_name = app.get_app_filename().split(".")[-1]
112
+ app_version = app.get_app_version().split("-")[0]
113
+ # and where it is installed
114
+ appdir = os.path.join(appdir, f"{app_name}-{app_version}")
115
+ except ModuleNotFoundError:
116
+ # Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
117
+ target = None
118
+ target_version = None
119
+ for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
120
+ test_dir = os.path.dirname(d)
121
+ # the name will be something like "create-2023.2.3"
122
+ name = os.path.basename(test_dir).split("-")
123
+ if len(name) != 2:
124
+ continue
125
+ if name[0] not in ("kit", "create", "view"):
126
+ continue
127
+ if (target_version is None) or (name[1] > target_version):
128
+ target = test_dir
129
+ target_version = name[1]
130
+ if target is None:
131
+ return None
132
+ appdir = target
133
+
134
+ # Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
135
+ # Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
136
+ exe_names = ["kit.sh", "kit"]
137
+ if sys.platform.startswith("win"):
138
+ exe_names = ["kit.bat", "kit.exe"]
139
+
140
+ # look in 4 places...
141
+ for dir_name in [appdir, os.path.join(appdir, "kit")]:
142
+ for exe_name in exe_names:
143
+ if os.path.exists(os.path.join(dir_name, exe_name)):
144
+ return os.path.join(dir_name, exe_name)
145
+
146
+ return None
147
+
148
+
149
+ def launch_kit_instance(
150
+ kit_path: Optional[str] = None,
151
+ extension_paths: Optional[List[str]] = None,
152
+ extensions: Optional[List[str]] = None,
153
+ cli_options: Optional[List[str]] = None,
154
+ log_file: Optional[str] = None,
155
+ log_level: str = "warn",
156
+ ) -> "OmniverseKitInstance":
157
+ """Launch an Omniverse application instance
158
+
159
+ Parameters
160
+ ----------
161
+ kit_path : Optional[str]
162
+ The full pathname of to a binary capable of serving as a kit runner.
163
+ extension_paths : Optional[List[str]]
164
+ List of directory names to include the in search for kits.
165
+ extensions : Optional[List[str]]
166
+ List of kit extensions to be loaded into the launched kit instance.
167
+ log_file : Optional[str]
168
+ The name of a text file where the logging information for the instance will be saved.
169
+ log_level : str
170
+ The level of the logging information to record: "verbose", "info", "warn", "error", "fatal",
171
+ the default is "warn".
172
+
173
+ Returns
174
+ -------
175
+ OmniverseKitInstance
176
+ The object interface for the launched instance
177
+
178
+ Examples
179
+ --------
180
+ Run a simple, empty GUI kit instance.
181
+
182
+ >>> from ansys.pyensight.core.utils import omniverse
183
+ >>> ov = omniverse.launch_kit_instance(extensions=['omni.kit.uiapp'])
184
+
185
+ """
186
+ # build the command line
187
+ if not kit_path:
188
+ kit_path = find_kit_filename()
189
+ if not kit_path:
190
+ raise RuntimeError("Unable to find a suitable Omniverse kit install")
191
+ cmd = [kit_path]
192
+ if extension_paths:
193
+ for path in extension_paths:
194
+ cmd.extend(["--ext-folder", path])
195
+ if extensions:
196
+ for ext in extensions:
197
+ cmd.extend(["--enable", ext])
198
+ if cli_options:
199
+ for opt in cli_options:
200
+ cmd.append(opt)
201
+ if log_level not in ("verbose", "info", "warn", "error", "fatal"):
202
+ raise RuntimeError(f"Invalid logging level: {log_level}")
203
+ cmd.append(f"--/log/level={log_level}")
204
+ if log_file:
205
+ cmd.append(f"--/log/file={log_file}")
206
+ cmd.append("--/log/enabled=true")
207
+ # Launch the process
208
+ env_vars = os.environ.copy()
209
+ p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env_vars)
210
+ return OmniverseKitInstance(p.pid)
211
+
212
+
21
213
  class Omniverse:
22
214
  """Provides the ``ensight.utils.omniverse`` interface.
23
215
 
@@ -56,79 +248,6 @@ class Omniverse:
56
248
  self._interpreter: str = ""
57
249
  self._status_filename: str = ""
58
250
 
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
251
  def _check_modules(self) -> None:
133
252
  """Verify that the Python interpreter is correct
134
253
 
@@ -361,9 +480,15 @@ class Omniverse:
361
480
  update_cmd += f"{prefix}timesteps=1"
362
481
  prefix = "&"
363
482
  if line_width != 0.0:
364
- # only in 2025 R2 and beyond
365
- if self._ensight._session.ensight_version_check("2025 R2", exception=False):
366
- update_cmd += f"{prefix}line_width={line_width}"
483
+ add_linewidth = False
484
+ if isinstance(self._ensight, ModuleType):
485
+ add_linewidth = True
486
+ else:
487
+ # only in 2025 R2 and beyond
488
+ if self._ensight._session.ensight_version_check("2025 R2", exception=False):
489
+ add_linewidth = True
490
+ if add_linewidth:
491
+ update_cmd += f"{prefix}ANSYS_linewidth={line_width}"
367
492
  prefix = "&"
368
493
  self._check_modules()
369
494
  if not self.is_running_omniverse():
@@ -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.
@@ -215,6 +209,11 @@ class OmniverseWrapper(object):
215
209
  ord(","): "_",
216
210
  ord(" "): "_",
217
211
  ord("\\"): "_",
212
+ ord("^"): "_",
213
+ ord("!"): "_",
214
+ ord("#"): "_",
215
+ ord("%"): "_",
216
+ ord("&"): "_",
218
217
  }
219
218
  name = name.translate(replacements)
220
219
  if name[0].isdigit():
@@ -582,7 +581,8 @@ class OmniverseWrapper(object):
582
581
  pbrShader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(color)
583
582
 
584
583
  material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface")
585
- UsdShade.MaterialBindingAPI(mesh).Bind(material)
584
+ mat_binding_api = UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim())
585
+ mat_binding_api.Bind(material)
586
586
 
587
587
  return material
588
588
 
@@ -639,7 +639,8 @@ class OmniverseWrapper(object):
639
639
  cam = geom_cam.GetCamera()
640
640
  # LOL, not sure why is might be correct, but so far it seems to work???
641
641
  cam.focalLength = camera.fieldofview
642
- cam.clippingRange = Gf.Range1f(0.1, 10)
642
+ dist = (target_pos - cam_pos).GetLength()
643
+ cam.clippingRange = Gf.Range1f(0.1 * dist, 10.0 * dist)
643
644
  look_at = Gf.Matrix4d()
644
645
  look_at.SetLookAt(cam_pos, target_pos, up_vec)
645
646
  trans_row = look_at.GetRow(3)
@@ -723,14 +724,83 @@ class OmniverseUpdateHandler(UpdateHandler):
723
724
  self._group_prims: Dict[int, Any] = dict()
724
725
  self._root_prim = None
725
726
  self._sent_textures = False
727
+ self._updated_camera = False
726
728
 
727
729
  def add_group(self, id: int, view: bool = False) -> None:
728
730
  super().add_group(id, view)
729
731
  group = self.session.groups[id]
732
+
730
733
  if not view:
734
+ # Capture changes in line/sphere sizes if it was not set from cli
735
+ width = self.get_dsg_cmd_attribute(group, "ANSYS_linewidth")
736
+ if width:
737
+ try:
738
+ self._omni.line_width = float(width)
739
+ except ValueError:
740
+ pass
741
+
731
742
  parent_prim = self._group_prims[group.parent_id]
743
+ # get the EnSight object type and the transform matrix
732
744
  obj_type = self.get_dsg_cmd_attribute(group, "ENS_OBJ_TYPE")
733
- matrix = self.group_matrix(group)
745
+ matrix = group.matrix4x4
746
+ # Is this a "case" group (it will contain part of the camera view in the matrix)
747
+ if obj_type == "ENS_CASE":
748
+ if (not self.session.vrmode) and (not self._updated_camera):
749
+ # if in camera mode, we need to update the camera matrix so we can
750
+ # use the identity matrix on this group. The camera should have been
751
+ # created in the "view" handler
752
+ cam_name = "/Root/Cam"
753
+ cam_prim = self._omni._stage.GetPrimAtPath(cam_name) # type: ignore
754
+ geom_cam = UsdGeom.Camera(cam_prim)
755
+ # get the camera
756
+ cam = geom_cam.GetCamera()
757
+ c = cam.transform
758
+ m = Gf.Matrix4d(*matrix).GetTranspose()
759
+ # move the model transform to the camera transform
760
+ cam.transform = c * m.GetInverse()
761
+ # set the updated camera
762
+ geom_cam.SetFromCamera(cam)
763
+ # apply the inverse cam transform to move the center of interest
764
+ # from data space to camera space
765
+ coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
766
+ if coi_attr.IsValid():
767
+ coi_data = coi_attr.Get()
768
+ coi_cam = (
769
+ Gf.Vec4d(coi_data[0], coi_data[1], coi_data[2], 1.0)
770
+ * cam.transform.GetInverse()
771
+ )
772
+ coi_attr.Set(
773
+ Gf.Vec3d(
774
+ coi_cam[0] / coi_cam[3],
775
+ coi_cam[1] / coi_cam[3],
776
+ coi_cam[2] / coi_cam[3],
777
+ )
778
+ )
779
+ # use the camera view by default
780
+ self._omni._stage.GetRootLayer().customLayerData = { # type: ignore
781
+ "cameraSettings": {"boundCamera": "/Root/Cam"}
782
+ }
783
+
784
+ # We only want to do this once
785
+ self._updated_camera = True
786
+ matrix = [
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
+ 0.0,
799
+ 0.0,
800
+ 0.0,
801
+ 0.0,
802
+ 1.0,
803
+ ]
734
804
  prim = self._omni.create_dsg_group(
735
805
  group.name, parent_prim, matrix=matrix, obj_type=obj_type
736
806
  )
@@ -801,40 +871,37 @@ class OmniverseUpdateHandler(UpdateHandler):
801
871
  first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
802
872
  mat_info=mat_info,
803
873
  )
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
- )
874
+ command, verts, tcoords, var_cmd = part.line_rep()
875
+ if command is not None:
876
+ # If there are no triangle (ideally if these are not hidden line
877
+ # edges), then use the base color for the part. If there are
878
+ # triangles, then assume these are hidden line edges and use the
879
+ # line_color.
880
+ line_color = color
881
+ if has_triangles:
882
+ line_color = [
883
+ part.cmd.line_color[0] * part.cmd.diffuse,
884
+ part.cmd.line_color[1] * part.cmd.diffuse,
885
+ part.cmd.line_color[2] * part.cmd.diffuse,
886
+ part.cmd.line_color[3],
887
+ ]
888
+ # TODO: texture coordinates on lines are current invalid in OV
889
+ var_cmd = None
890
+ tcoords = None
891
+ # Generate the lines
892
+ _ = self._omni.create_dsg_lines(
893
+ name,
894
+ obj_id,
895
+ part.hash,
896
+ parent_prim,
897
+ verts,
898
+ tcoords,
899
+ matrix=matrix,
900
+ diffuse=line_color,
901
+ variable=var_cmd,
902
+ timeline=self.session.cur_timeline,
903
+ first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
904
+ )
838
905
 
839
906
  elif part.cmd.render == part.cmd.NODES:
840
907
  command, verts, sizes, colors, var_cmd = part.point_rep()
@@ -875,6 +942,8 @@ class OmniverseUpdateHandler(UpdateHandler):
875
942
  # Upload a material to the Omniverse server
876
943
  self._omni.uploadMaterial()
877
944
  self._sent_textures = False
945
+ # We want to update the camera a single time within this update
946
+ self._updated_camera = False
878
947
 
879
948
  def end_update(self) -> None:
880
949
  super().end_update()