ansys-pyensight-core 0.8.0__py3-none-any.whl → 0.8.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.

@@ -28,25 +28,28 @@ import argparse
28
28
  import logging
29
29
  import math
30
30
  import os
31
- import queue
32
31
  import shutil
33
32
  import sys
34
- import threading
35
- from typing import Any, List, Optional
33
+ from typing import Any, Dict, List, Optional
36
34
 
37
- from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
38
- from ansys.pyensight.core import ensight_grpc
39
- import numpy
40
35
  import omni.client
41
36
  import png
42
37
  from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux, UsdShade
43
38
 
39
+ sys.path.append(os.path.dirname(__file__))
40
+ from dsg_server import DSGSession, Part, UpdateHandler # noqa: E402
41
+
44
42
 
45
43
  class OmniverseWrapper:
46
44
  verbose = 0
47
45
 
48
46
  @staticmethod
49
47
  def logCallback(threadName: None, component: Any, level: Any, message: str) -> None:
48
+ """
49
+ The logger method registered to handle async messages from Omniverse
50
+
51
+ If running in verbose mode, reroute the messages to Python Logging.
52
+ """
50
53
  if OmniverseWrapper.verbose:
51
54
  logging.info(message)
52
55
 
@@ -54,6 +57,9 @@ class OmniverseWrapper:
54
57
  def connectionStatusCallback(
55
58
  url: Any, connectionStatus: "omni.client.ConnectionStatus"
56
59
  ) -> None:
60
+ """
61
+ If no connection to Omniverse can be made, shut down the service.
62
+ """
57
63
  if connectionStatus is omni.client.ConnectionStatus.CONNECT_ERROR:
58
64
  sys.exit("[ERROR] Failed connection, exiting.")
59
65
 
@@ -62,15 +68,15 @@ class OmniverseWrapper:
62
68
  live_edit: bool = False,
63
69
  path: str = "omniverse://localhost/Users/test",
64
70
  verbose: int = 0,
65
- ):
71
+ ) -> None:
66
72
  self._cleaned_index = 0
67
73
  self._cleaned_names: dict = {}
68
74
  self._connectionStatusSubscription = None
69
75
  self._stage = None
70
- self._destinationPath = path
76
+ self._destinationPath: str = path
71
77
  self._old_stages: list = []
72
78
  self._stagename = "dsg_scene.usd"
73
- self._live_edit = live_edit
79
+ self._live_edit: bool = live_edit
74
80
  if self._live_edit:
75
81
  self._stagename = "dsg_scene.live"
76
82
  OmniverseWrapper.verbose = verbose
@@ -90,10 +96,16 @@ class OmniverseWrapper:
90
96
  self.log("Note technically the Omniverse URL {self._destinationPath} is not valid")
91
97
 
92
98
  def log(self, msg: str) -> None:
99
+ """
100
+ Local method to dispatch to whatever logging system has been enabled.
101
+ """
93
102
  if OmniverseWrapper.verbose:
94
103
  logging.info(msg)
95
104
 
96
105
  def shutdown(self) -> None:
106
+ """
107
+ Shutdown the connection to Omniverse cleanly.
108
+ """
97
109
  omni.client.live_wait_for_pending_updates()
98
110
  self._connectionStatusSubscription = None
99
111
  omni.client.shutdown()
@@ -106,37 +118,67 @@ class OmniverseWrapper:
106
118
  return False
107
119
 
108
120
  def stage_url(self, name: Optional[str] = None) -> str:
121
+ """
122
+ For a given object name, create the URL for the item.
123
+ Parameters
124
+ ----------
125
+ name: the name of the object to generate the URL for. If None, it will be the URL for the
126
+ stage name.
127
+
128
+ Returns
129
+ -------
130
+ The URL for the object.
131
+ """
109
132
  if name is None:
110
133
  name = self._stagename
111
134
  return self._destinationPath + "/" + name
112
135
 
113
136
  def delete_old_stages(self) -> None:
137
+ """
138
+ Remove all the stages included in the "_old_stages" list.
139
+ """
114
140
  while self._old_stages:
115
141
  stage = self._old_stages.pop()
116
142
  omni.client.delete(stage)
117
143
 
118
144
  def create_new_stage(self) -> None:
145
+ """
146
+ Create a new stage. using the current stage name.
147
+ """
119
148
  self.log(f"Creating Omniverse stage: {self.stage_url()}")
120
149
  if self._stage:
121
150
  self._stage.Unload()
122
151
  self._stage = None
123
152
  self.delete_old_stages()
124
153
  self._stage = Usd.Stage.CreateNew(self.stage_url())
154
+ # record the stage in the "_old_stages" list.
125
155
  self._old_stages.append(self.stage_url())
126
156
  UsdGeom.SetStageUpAxis(self._stage, UsdGeom.Tokens.y)
127
157
  # in M
128
158
  UsdGeom.SetStageMetersPerUnit(self._stage, 1.0)
129
159
  self.log(f"Created stage: {self.stage_url()}")
130
160
 
131
- def save_stage(self) -> None:
161
+ def save_stage(self, comment: str = "") -> None:
162
+ """
163
+ For live connections, save the current edit and allow live processing.
164
+
165
+ Presently, live connections are disabled.
166
+ """
132
167
  self._stage.GetRootLayer().Save() # type:ignore
133
168
  omni.client.live_process()
134
169
 
135
- # This function will add a commented checkpoint to a file on Nucleus if:
136
- # Live mode is disabled (live checkpoints are ill-supported)
137
- # The Nucleus server supports checkpoints
170
+ # This function will add a commented checkpoint to a file on Nucleus if
171
+ # the Nucleus server supports checkpoints
138
172
  def checkpoint(self, comment: str = "") -> None:
139
- if self._live_edit:
173
+ """
174
+ Add a checkpoint to the current stage.
175
+
176
+ Parameters
177
+ ----------
178
+ comment: str
179
+ If not empty, the comment to be added to the stage
180
+ """
181
+ if not comment:
140
182
  return
141
183
  result, serverInfo = omni.client.get_server_info(self.stage_url())
142
184
  if result and serverInfo and serverInfo.checkpoints_enabled:
@@ -145,6 +187,17 @@ class OmniverseWrapper:
145
187
  omni.client.create_checkpoint(self.stage_url(), comment, bForceCheckpoint)
146
188
 
147
189
  def username(self, display: bool = True) -> Optional[str]:
190
+ """
191
+ Get the username of the current user.
192
+
193
+ Parameters
194
+ ----------
195
+ display : bool, optional if True, send the username to the logging system.
196
+
197
+ Returns
198
+ -------
199
+ The username or None.
200
+ """
148
201
  result, serverInfo = omni.client.get_server_info(self.stage_url())
149
202
  if serverInfo:
150
203
  if display:
@@ -152,162 +205,15 @@ class OmniverseWrapper:
152
205
  return serverInfo.username
153
206
  return None
154
207
 
155
- h = 50.0
156
- boxVertexIndices = [
157
- 0,
158
- 1,
159
- 2,
160
- 1,
161
- 3,
162
- 2,
163
- 4,
164
- 5,
165
- 6,
166
- 4,
167
- 6,
168
- 7,
169
- 8,
170
- 9,
171
- 10,
172
- 8,
173
- 10,
174
- 11,
175
- 12,
176
- 13,
177
- 14,
178
- 12,
179
- 14,
180
- 15,
181
- 16,
182
- 17,
183
- 18,
184
- 16,
185
- 18,
186
- 19,
187
- 20,
188
- 21,
189
- 22,
190
- 20,
191
- 22,
192
- 23,
193
- ]
194
- boxVertexCounts = [3] * 12
195
- boxNormals = [
196
- (0, 0, -1),
197
- (0, 0, -1),
198
- (0, 0, -1),
199
- (0, 0, -1),
200
- (0, 0, 1),
201
- (0, 0, 1),
202
- (0, 0, 1),
203
- (0, 0, 1),
204
- (0, -1, 0),
205
- (0, -1, 0),
206
- (0, -1, 0),
207
- (0, -1, 0),
208
- (1, 0, 0),
209
- (1, 0, 0),
210
- (1, 0, 0),
211
- (1, 0, 0),
212
- (0, 1, 0),
213
- (0, 1, 0),
214
- (0, 1, 0),
215
- (0, 1, 0),
216
- (-1, 0, 0),
217
- (-1, 0, 0),
218
- (-1, 0, 0),
219
- (-1, 0, 0),
220
- ]
221
- boxPoints = [
222
- (h, -h, -h),
223
- (-h, -h, -h),
224
- (h, h, -h),
225
- (-h, h, -h),
226
- (h, h, h),
227
- (-h, h, h),
228
- (-h, -h, h),
229
- (h, -h, h),
230
- (h, -h, h),
231
- (-h, -h, h),
232
- (-h, -h, -h),
233
- (h, -h, -h),
234
- (h, h, h),
235
- (h, -h, h),
236
- (h, -h, -h),
237
- (h, h, -h),
238
- (-h, h, h),
239
- (h, h, h),
240
- (h, h, -h),
241
- (-h, h, -h),
242
- (-h, -h, h),
243
- (-h, h, h),
244
- (-h, h, -h),
245
- (-h, -h, -h),
246
- ]
247
- boxUVs = [
248
- (0, 0),
249
- (0, 1),
250
- (1, 1),
251
- (1, 0),
252
- (0, 0),
253
- (0, 1),
254
- (1, 1),
255
- (1, 0),
256
- (0, 0),
257
- (0, 1),
258
- (1, 1),
259
- (1, 0),
260
- (0, 0),
261
- (0, 1),
262
- (1, 1),
263
- (1, 0),
264
- (0, 0),
265
- (0, 1),
266
- (1, 1),
267
- (1, 0),
268
- (0, 0),
269
- (0, 1),
270
- (1, 1),
271
- (1, 0),
272
- ]
273
-
274
- def createBox(self, box_number: int = 0) -> "UsdGeom.Mesh":
275
- rootUrl = "/Root"
276
- boxUrl = rootUrl + "/Boxes/box_%d" % box_number
277
- xformPrim = UsdGeom.Xform.Define(self._stage, rootUrl) # noqa: F841
278
- # Define the defaultPrim as the /Root prim
279
- rootPrim = self._stage.GetPrimAtPath(rootUrl) # type:ignore
280
- self._stage.SetDefaultPrim(rootPrim) # type:ignore
281
- boxPrim = UsdGeom.Mesh.Define(self._stage, boxUrl)
282
- boxPrim.CreateDisplayColorAttr([(0.463, 0.725, 0.0)])
283
- boxPrim.CreatePointsAttr(OmniverseWrapper.boxPoints)
284
- boxPrim.CreateNormalsAttr(OmniverseWrapper.boxNormals)
285
- boxPrim.CreateFaceVertexCountsAttr(OmniverseWrapper.boxVertexCounts)
286
- boxPrim.CreateFaceVertexIndicesAttr(OmniverseWrapper.boxVertexIndices)
287
- # USD 22.08 changed the primvar API
288
- if hasattr(boxPrim, "CreatePrimvar"):
289
- texCoords = boxPrim.CreatePrimvar(
290
- "st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
291
- )
292
- else:
293
- primvarsAPI = UsdGeom.PrimvarsAPI(boxPrim)
294
- texCoords = primvarsAPI.CreatePrimvar(
295
- "st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
296
- )
297
- texCoords.Set(OmniverseWrapper.boxUVs)
298
- texCoords.SetInterpolation("vertex")
299
- if not boxPrim:
300
- sys.exit("[ERROR] Failure to create box")
301
- self.save_stage()
302
- return boxPrim
303
-
304
208
  def clear_cleaned_names(self) -> None:
305
- """Clear the list of cleaned names"""
209
+ """
210
+ Clear the list of cleaned names
211
+ """
306
212
  self._cleaned_names = {}
307
213
  self._cleaned_index = 0
308
214
 
309
215
  def clean_name(self, name: str, id_name: Any = None) -> str:
310
- """Generate a vais USD name
216
+ """Generate a valid USD name
311
217
 
312
218
  From a base (EnSight) varname, partname, etc. and the DSG id, generate
313
219
  a unique, valid USD name. Save the names so that if the same name
@@ -329,7 +235,7 @@ class OmniverseWrapper:
329
235
  # return any previously generated name
330
236
  if (name, id_name) in self._cleaned_names:
331
237
  return self._cleaned_names[(name, id_name)]
332
- # replace invalid characters
238
+ # replace invalid characters. EnSight uses a number of characters that are illegal in USD names.
333
239
  name = name.replace("+", "_").replace("-", "_")
334
240
  name = name.replace(".", "_").replace(":", "_")
335
241
  name = name.replace("[", "_").replace("]", "_")
@@ -351,6 +257,17 @@ class OmniverseWrapper:
351
257
 
352
258
  @staticmethod
353
259
  def decompose_matrix(values: Any) -> Any:
260
+ """
261
+ Decompose an array of floats (representing a 4x4 matrix) into scale, rotation and translation.
262
+ Parameters
263
+ ----------
264
+ values:
265
+ 16 values (input to Gf.Matrix4f CTOR)
266
+
267
+ Returns
268
+ -------
269
+ (scale, rotation, translation)
270
+ """
354
271
  # ang_convert = 180.0/math.pi
355
272
  ang_convert = 1.0
356
273
  trans_convert = 1.0
@@ -656,8 +573,6 @@ class OmniverseWrapper:
656
573
 
657
574
  UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim()).Bind(newMat)
658
575
 
659
- # self.save_stage()
660
-
661
576
  # Create a distant light in the scene.
662
577
  def createDistantLight(self):
663
578
  newLight = UsdLux.DistantLight.Define(self._stage, "/Root/DistantLight")
@@ -665,8 +580,6 @@ class OmniverseWrapper:
665
580
  newLight.CreateColorAttr(Gf.Vec3f(1.0, 1.0, 0.745))
666
581
  newLight.CreateIntensityAttr(500.0)
667
582
 
668
- # self.save_stage()
669
-
670
583
  # Create a dome light in the scene.
671
584
  def createDomeLight(self, texturePath):
672
585
  newLight = UsdLux.DomeLight.Define(self._stage, "/Root/DomeLight")
@@ -679,8 +592,6 @@ class OmniverseWrapper:
679
592
  rotateOp = xForm.AddXformOp(UsdGeom.XformOp.TypeRotateZYX, UsdGeom.XformOp.PrecisionFloat)
680
593
  rotateOp.Set(Gf.Vec3f(270, 0, 0))
681
594
 
682
- # self.save_stage()
683
-
684
595
  def createEmptyFolder(self, emptyFolderPath):
685
596
  folder = self._destinationPath + emptyFolderPath
686
597
  self.log(f"Creating new folder: {folder}")
@@ -689,531 +600,95 @@ class OmniverseWrapper:
689
600
  return result.name
690
601
 
691
602
 
692
- class Part(object):
693
- def __init__(self, link: "DSGOmniverseLink"):
694
- self._link = link
695
- self.cmd: Optional[Any] = None
696
- self.reset()
697
-
698
- def reset(self, cmd: Any = None) -> None:
699
- self.conn_tris = numpy.array([], dtype="int32")
700
- self.conn_lines = numpy.array([], dtype="int32")
701
- self.coords = numpy.array([], dtype="float32")
702
- self.normals = numpy.array([], dtype="float32")
703
- self.normals_elem = False
704
- self.tcoords = numpy.array([], dtype="float32")
705
- self.tcoords_var = None
706
- self.tcoords_elem = False
707
- self.cmd = cmd
708
-
709
- def update_geom(self, cmd: dynamic_scene_graph_pb2.UpdateGeom) -> None:
710
- if cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.COORDINATES:
711
- if self.coords.size != cmd.total_array_size:
712
- self.coords = numpy.resize(self.coords, cmd.total_array_size)
713
- self.coords[cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)] = cmd.flt_array
714
- elif cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.TRIANGLES:
715
- if self.conn_tris.size != cmd.total_array_size:
716
- self.conn_tris = numpy.resize(self.conn_tris, cmd.total_array_size)
717
- self.conn_tris[cmd.chunk_offset : cmd.chunk_offset + len(cmd.int_array)] = cmd.int_array
718
- elif cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.LINES:
719
- if self.conn_lines.size != cmd.total_array_size:
720
- self.conn_lines = numpy.resize(self.conn_lines, cmd.total_array_size)
721
- self.conn_lines[
722
- cmd.chunk_offset : cmd.chunk_offset + len(cmd.int_array)
723
- ] = cmd.int_array
724
- elif (cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_NORMALS) or (
725
- cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.NODE_NORMALS
726
- ):
727
- self.normals_elem = cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_NORMALS
728
- if self.normals.size != cmd.total_array_size:
729
- self.normals = numpy.resize(self.normals, cmd.total_array_size)
730
- self.normals[cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)] = cmd.flt_array
731
- elif (cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE) or (
732
- cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.NODE_VARIABLE
733
- ):
734
- # Get the variable definition
735
- if cmd.variable_id in self._link._variables:
736
- self.tcoords_var = cmd.variable_id
737
- self.tcoords_elem = (
738
- cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE
739
- )
740
- if self.tcoords.size != cmd.total_array_size:
741
- self.tcoords = numpy.resize(self.tcoords, cmd.total_array_size)
742
- self.tcoords[
743
- cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)
744
- ] = cmd.flt_array
745
- else:
746
- self.tcoords_var = None
603
+ class OmniverseUpdateHandler(UpdateHandler):
604
+ """
605
+ Implement the Omniverse glue to a DSGSession instance
606
+ """
747
607
 
748
- def build(self):
749
- if self.cmd is None:
750
- return
751
- if self.conn_lines.size:
752
- self._link.log(
753
- f"Note, part '{self.cmd.name}' has lines which are not currently supported."
608
+ def __init__(self, omni: OmniverseWrapper):
609
+ super().__init__()
610
+ self._omni = omni
611
+ self._group_prims: Dict[int, Any] = dict()
612
+
613
+ def add_group(self, id: int, view: bool = False) -> None:
614
+ super().add_group(id, view)
615
+ group = self.session.groups[id]
616
+ if not view:
617
+ parent_prim = self._group_prims[group.parent_id]
618
+ obj_type = self.get_dsg_cmd_attribute(group, "ENS_OBJ_TYPE")
619
+ matrix = self.group_matrix(group)
620
+ prim = self._omni.create_dsg_group(
621
+ group.name, parent_prim, matrix=matrix, obj_type=obj_type
754
622
  )
755
- self.cmd = None
623
+ self._group_prims[id] = prim
624
+ else:
625
+ # Map a view command into a new Omniverse stage and populate it with materials/lights.
626
+ # Create a new root stage in Omniverse
627
+ self._omni.create_new_stage()
628
+ # Create the root group/camera
629
+ camera_info = group
630
+ if self.session.vrmode:
631
+ camera_info = None
632
+ prim = self._omni.create_dsg_root(camera=camera_info)
633
+ # Create a distance and dome light in the scene
634
+ self._omni.createDomeLight("./Materials/000_sky.exr")
635
+ # Upload a material and textures to the Omniverse server
636
+ self._omni.uploadMaterial()
637
+ self._omni.create_dsg_variable_textures(self.session.variables)
638
+ # record
639
+ self._group_prims[id] = prim
640
+
641
+ def add_variable(self, id: int) -> None:
642
+ super().add_variable(id)
643
+
644
+ def finalize_part(self, part: Part) -> None:
645
+ # generate an Omniverse compliant mesh from the Part
646
+ command, verts, conn, normals, tcoords, var_cmd = part.build()
647
+ if command is None:
756
648
  return
757
- verts = self.coords
758
- if self._link._normalize_geometry and self._link._scene_bounds is not None:
759
- midx = (self._link._scene_bounds[3] + self._link._scene_bounds[0]) * 0.5
760
- midy = (self._link._scene_bounds[4] + self._link._scene_bounds[1]) * 0.5
761
- midz = (self._link._scene_bounds[5] + self._link._scene_bounds[2]) * 0.5
762
- dx = self._link._scene_bounds[3] - self._link._scene_bounds[0]
763
- dy = self._link._scene_bounds[4] - self._link._scene_bounds[1]
764
- dz = self._link._scene_bounds[5] - self._link._scene_bounds[2]
765
- s = dx
766
- if dy > s:
767
- s = dy
768
- if dz > s:
769
- s = dz
770
- if s == 0:
771
- s = 1.0
772
- num_verts = int(verts.size / 3)
773
- for i in range(num_verts):
774
- j = i * 3
775
- verts[j + 0] = (verts[j + 0] - midx) / s
776
- verts[j + 1] = (verts[j + 1] - midy) / s
777
- verts[j + 2] = (verts[j + 2] - midz) / s
778
-
779
- conn = self.conn_tris
780
- normals = self.normals
781
- tcoords = None
782
- if self.tcoords.size:
783
- tcoords = self.tcoords
784
- if self.tcoords_elem or self.normals_elem:
785
- verts_per_prim = 3
786
- num_prims = int(conn.size / verts_per_prim)
787
- # "flatten" the triangles to move values from elements to nodes
788
- new_verts = numpy.ndarray((num_prims * verts_per_prim * 3,), dtype="float32")
789
- new_conn = numpy.ndarray((num_prims * verts_per_prim,), dtype="int32")
790
- new_tcoords = None
791
- if tcoords is not None:
792
- # remember that the input values are 1D at this point, we will expand to 2D later
793
- new_tcoords = numpy.ndarray((num_prims * verts_per_prim,), dtype="float32")
794
- new_normals = None
795
- if normals is not None:
796
- if normals.size == 0:
797
- print("Warning: zero length normals!")
798
- else:
799
- new_normals = numpy.ndarray((num_prims * verts_per_prim * 3,), dtype="float32")
800
- j = 0
801
- for i0 in range(num_prims):
802
- for i1 in range(verts_per_prim):
803
- idx = conn[i0 * verts_per_prim + i1]
804
- # new connectivity (identity)
805
- new_conn[j] = j
806
- # copy the vertex
807
- new_verts[j * 3 + 0] = verts[idx * 3 + 0]
808
- new_verts[j * 3 + 1] = verts[idx * 3 + 1]
809
- new_verts[j * 3 + 2] = verts[idx * 3 + 2]
810
- if new_normals is not None:
811
- if self.normals_elem:
812
- # copy the normal associated with the face
813
- new_normals[j * 3 + 0] = normals[i0 * 3 + 0]
814
- new_normals[j * 3 + 1] = normals[i0 * 3 + 1]
815
- new_normals[j * 3 + 2] = normals[i0 * 3 + 2]
816
- else:
817
- # copy the same normal as the vertex
818
- new_normals[j * 3 + 0] = normals[idx * 3 + 0]
819
- new_normals[j * 3 + 1] = normals[idx * 3 + 1]
820
- new_normals[j * 3 + 2] = normals[idx * 3 + 2]
821
- if new_tcoords is not None:
822
- # remember, 1D texture coords at this point
823
- if self.tcoords_elem:
824
- # copy the texture coord associated with the face
825
- new_tcoords[j] = tcoords[i0]
826
- else:
827
- # copy the same texture coord as the vertex
828
- new_tcoords[j] = tcoords[idx]
829
- j += 1
830
- # new arrays.
831
- verts = new_verts
832
- conn = new_conn
833
- normals = new_normals
834
- if tcoords is not None:
835
- tcoords = new_tcoords
836
-
837
- var = None
838
- # texture coords need transformation from variable value to [ST]
839
- if tcoords is not None:
840
- var_id = self.cmd.color_variableid
841
- var = self._link._variables[var_id]
842
- v_min = None
843
- v_max = None
844
- for lvl in var.levels:
845
- if (v_min is None) or (v_min > lvl.value):
846
- v_min = lvl.value
847
- if (v_max is None) or (v_max < lvl.value):
848
- v_max = lvl.value
849
- var_minmax = [v_min, v_max]
850
- # build a power of two x 1 texture
851
- num_texels = int(len(var.texture) / 4)
852
- half_texel = 1 / (num_texels * 2.0)
853
- num_verts = int(verts.size / 3)
854
- tmp = numpy.ndarray((num_verts * 2,), dtype="float32")
855
- tmp.fill(0.5) # fill in the T coordinate...
856
- tex_width = half_texel * 2 * (num_texels - 1) # center to center of num_texels
857
- # if the range is 0, adjust the min by -1. The result is that the texture
858
- # coords will get mapped to S=1.0 which is what EnSight does in this situation
859
- if (var_minmax[1] - var_minmax[0]) == 0.0:
860
- var_minmax[0] = var_minmax[0] - 1.0
861
- var_width = var_minmax[1] - var_minmax[0]
862
- for idx in range(num_verts):
863
- # normalized S coord value (clamp)
864
- s = (tcoords[idx] - var_minmax[0]) / var_width
865
- if s < 0.0:
866
- s = 0.0
867
- if s > 1.0:
868
- s = 1.0
869
- # map to the texture range and set the S value
870
- tmp[idx * 2] = s * tex_width + half_texel
871
- tcoords = tmp
872
-
873
- parent = self._link._groups[self.cmd.parent_id]
649
+ parent_prim = self._group_prims[command.parent_id]
650
+ obj_id = self._session.mesh_block_count
651
+ matrix = command.matrix4x4
652
+ name = command.name
874
653
  color = [
875
- self.cmd.fill_color[0] * self.cmd.diffuse,
876
- self.cmd.fill_color[1] * self.cmd.diffuse,
877
- self.cmd.fill_color[2] * self.cmd.diffuse,
878
- self.cmd.fill_color[3],
654
+ command.fill_color[0] * command.diffuse,
655
+ command.fill_color[1] * command.diffuse,
656
+ command.fill_color[2] * command.diffuse,
657
+ command.fill_color[3],
879
658
  ]
880
- obj_id = self._link._mesh_block_count
881
- # prim =
882
- _ = self._link._omni.create_dsg_mesh_block(
883
- self.cmd.name,
659
+ # Generate the mesh block
660
+ _ = self._omni.create_dsg_mesh_block(
661
+ name,
884
662
  obj_id,
885
- parent[1],
663
+ parent_prim,
886
664
  verts,
887
665
  conn,
888
666
  normals,
889
667
  tcoords,
890
- matrix=self.cmd.matrix4x4,
668
+ matrix=matrix,
891
669
  diffuse=color,
892
- variable=var,
893
- )
894
- self._link.log(
895
- f"Part '{self.cmd.name}' defined: {self.coords.size/3} verts, {self.conn_tris.size/3} tris, {self.conn_lines.size/2} lines."
670
+ variable=var_cmd,
896
671
  )
897
- self.cmd = None
898
-
899
-
900
- class DSGOmniverseLink(object):
901
- def __init__(
902
- self,
903
- omni: OmniverseWrapper,
904
- port: int = 12345,
905
- host: str = "127.0.0.1",
906
- security_code: str = "",
907
- verbose: int = 0,
908
- normalize_geometry: bool = False,
909
- vrmode: bool = False,
910
- ):
911
- super().__init__()
912
- self._grpc = ensight_grpc.EnSightGRPC(port=port, host=host, secret_key=security_code)
913
- self._verbose = verbose
914
- self._thread: Optional[threading.Thread] = None
915
- self._message_queue: queue.Queue = queue.Queue() # Messages coming from EnSight
916
- self._dsg_queue: Optional[queue.SimpleQueue] = None # Outgoing messages to EnSight
917
- self._shutdown = False
918
- self._dsg = None
919
- self._omni = omni
920
- self._normalize_geometry = normalize_geometry
921
- self._vrmode = vrmode
922
- self._mesh_block_count = 0
923
- self._variables: dict = {}
924
- self._groups: dict = {}
925
- self._part: Part = Part(self)
926
- self._scene_bounds: Optional[List] = None
927
-
928
- def log(self, s: str) -> None:
929
- """Log a string to the logging system
930
-
931
- If verbosity is set, log the string.
932
- """
933
- if self._verbose > 0:
934
- logging.info(s)
672
+ super().finalize_part(part)
935
673
 
936
- def start(self) -> int:
937
- """Start a gRPC connection to an EnSight instance
674
+ def start_connection(self) -> None:
675
+ super().start_connection()
938
676
 
939
- Make a gRPC connection and start a DSG stream handler.
677
+ def end_connection(self) -> None:
678
+ super().end_connection()
940
679
 
941
- Returns
942
- -------
943
- 0 on success, -1 on an error.
944
- """
945
- # Start by setting up and verifying the connection
946
- self._grpc.connect()
947
- if not self._grpc.is_connected():
948
- logging.info(
949
- f"Unable to establish gRPC connection to: {self._grpc.host()}:{self._grpc.port()}"
950
- )
951
- return -1
952
- # Streaming API requires an iterator, so we make one from a queue
953
- # it also returns an iterator. self._dsg_queue is the input stream interface
954
- # self._dsg is the returned stream iterator.
955
- if self._dsg is not None:
956
- return 0
957
- self._dsg_queue = queue.SimpleQueue()
958
- self._dsg = self._grpc.dynamic_scene_graph_stream(
959
- iter(self._dsg_queue.get, None) # type:ignore
960
- )
961
- self._thread = threading.Thread(target=self.poll_messages)
962
- if self._thread is not None:
963
- self._thread.start()
964
- return 0
965
-
966
- def end(self):
967
- """Stop a gRPC connection to the EnSight instance"""
968
- self._grpc.stop_server()
969
- self._shutdown = True
970
- self._thread.join()
971
- self._grpc.shutdown()
972
- self._dsg = None
973
- self._thread = None
974
- self._dsg_queue = None
975
-
976
- def is_shutdown(self):
977
- """Check the service shutdown request status"""
978
- return self._shutdown
979
-
980
- def request_an_update(self, animation: bool = False) -> None:
981
- """Start a DSG update
982
- Send a command to the DSG protocol to "init" an update.
983
-
984
- Parameters
985
- ----------
986
- animation:
987
- if True, export all EnSight timesteps.
988
- """
989
- # Send an INIT command to trigger a stream of update packets
990
- cmd = dynamic_scene_graph_pb2.SceneClientCommand()
991
- cmd.command_type = dynamic_scene_graph_pb2.SceneClientCommand.INIT
992
- # Allow EnSight push commands, but full scene only for now...
993
- cmd.init.allow_spontaneous = True
994
- cmd.init.include_temporal_geometry = animation
995
- cmd.init.allow_incremental_updates = False
996
- cmd.init.maximum_chunk_size = 1024 * 1024
997
- self._dsg_queue.put(cmd) # type:ignore
998
- # Handle the update messages
999
- self.handle_one_update()
1000
-
1001
- def poll_messages(self) -> None:
1002
- """Core interface to grab DSG events from gRPC and queue them for processing
1003
-
1004
- This is run by a thread that is monitoring the dsg RPC call for update messages
1005
- it places them in _message_queue as it finds them. They are picked up by the
1006
- main thread via get_next_message()
1007
- """
1008
- while not self._shutdown:
1009
- try:
1010
- self._message_queue.put(next(self._dsg)) # type:ignore
1011
- except Exception:
1012
- self._shutdown = True
1013
- logging.info("DSG connection broken, calling exit")
1014
- os._exit(0)
1015
-
1016
- def get_next_message(self, wait: bool = True) -> Any:
1017
- """Get the next queued up protobuffer message
1018
-
1019
- Called by the main thread to get any messages that were pulled in from the
1020
- dsg stream and placed here by poll_messages()
1021
- """
1022
- try:
1023
- return self._message_queue.get(block=wait)
1024
- except queue.Empty:
1025
- return None
1026
-
1027
- def handle_one_update(self) -> None:
1028
- """Monitor the DSG stream and handle a single update operation
1029
-
1030
- Wait until we get the scene update begin message. From there, reset the current
1031
- scene buckets and then parse all the incoming commands until we get the scene
1032
- update end command. At which point, save the generated stage (started in the
1033
- view command handler). Note: Parts are handled with an available bucket at all times.
1034
- When a new part update comes in or the scene update end happens, the part is "finished".
1035
- """
1036
- # An update starts with a UPDATE_SCENE_BEGIN command
1037
- cmd = self.get_next_message()
1038
- while (cmd is not None) and (
1039
- cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_BEGIN
1040
- ):
1041
- # Look for a begin command
1042
- cmd = self.get_next_message()
1043
- self.log("Begin update ------------------------")
1044
-
1045
- # Start anew
1046
- self._variables = {}
1047
- self._groups = {}
1048
- self._part = Part(self)
1049
- self._scene_bounds = None
1050
- self._mesh_block_count = 0 # reset when a new group shows up
680
+ def begin_update(self) -> None:
681
+ super().begin_update()
682
+ # restart the name tables
1051
683
  self._omni.clear_cleaned_names()
684
+ # clear the group Omni prims list
685
+ self._group_prims = dict()
1052
686
 
1053
- # handle the various commands until UPDATE_SCENE_END
1054
- cmd = self.get_next_message()
1055
- while (cmd is not None) and (
1056
- cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_END
1057
- ):
1058
- self.handle_update_command(cmd)
1059
- cmd = self.get_next_message()
1060
-
1061
- # Flush the last part
1062
- self.finish_part()
1063
-
687
+ def end_update(self) -> None:
688
+ super().end_update()
1064
689
  # Stage update complete
1065
690
  self._omni.save_stage()
1066
691
 
1067
- self.log("End update --------------------------")
1068
-
1069
- # handle an incoming gRPC update command
1070
- def handle_update_command(self, cmd: dynamic_scene_graph_pb2.SceneUpdateCommand) -> None:
1071
- """Dispatch out a scene update command to the proper handler
1072
-
1073
- Given a command object, pull out the correct portion of the protobuffer union and
1074
- pass it to the appropriate handler.
1075
-
1076
- Parameters
1077
- ----------
1078
- cmd:
1079
- The command to be dispatched.
1080
- """
1081
- name = "Unknown"
1082
- if cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.DELETE_ID:
1083
- name = "Delete IDs"
1084
- elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_PART:
1085
- name = "Part update"
1086
- tmp = cmd.update_part
1087
- self.handle_part(tmp)
1088
- elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GROUP:
1089
- name = "Group update"
1090
- tmp = cmd.update_group
1091
- self.handle_group(tmp)
1092
- elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GEOM:
1093
- name = "Geom update"
1094
- tmp = cmd.update_geom
1095
- self._part.update_geom(tmp)
1096
- elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VARIABLE:
1097
- name = "Variable update"
1098
- tmp = cmd.update_variable
1099
- self.handle_variable(tmp)
1100
- elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VIEW:
1101
- name = "View update"
1102
- tmp = cmd.update_view
1103
- self.handle_view(tmp)
1104
- elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_TEXTURE:
1105
- name = "Texture update"
1106
- self.log(f"{name} --------------------------")
1107
-
1108
- def finish_part(self) -> None:
1109
- """Complete the current part
1110
-
1111
- There is always a part being modified. This method completes the current part, commits
1112
- it to the Omniverse USD, and sets up the next part.
1113
- """
1114
- self._part.build()
1115
- self._mesh_block_count += 1
1116
-
1117
- def handle_part(self, part: Any) -> None:
1118
- """Handle a DSG UPDATE_GROUP command
1119
- Parameters
1120
- ----------
1121
- part:
1122
- The command coming from the EnSight stream.
1123
- """
1124
- self.finish_part()
1125
- self._part.reset(part)
1126
-
1127
- def handle_group(self, group: Any) -> None:
1128
- """Handle a DSG UPDATE_GROUP command
1129
- Parameters
1130
- ----------
1131
- group:
1132
- The command coming from the EnSight stream.
1133
- """
1134
- # reset current mesh (part) count for unique "part" naming in USD
1135
- self._mesh_block_count = 0
1136
- # get the parent group or view
1137
- parent = self._groups[group.parent_id]
1138
- obj_type = group.attributes.get("ENS_OBJ_TYPE", None)
1139
- matrix = group.matrix4x4
1140
- # The Case matrix is basically the camera transform. In vrmode, we only want
1141
- # the raw geometry, so use the identity matrix.
1142
- if (obj_type == "ENS_CASE") and self._vrmode:
1143
- matrix = [
1144
- 1.0,
1145
- 0.0,
1146
- 0.0,
1147
- 0.0,
1148
- 0.0,
1149
- 1.0,
1150
- 0.0,
1151
- 0.0,
1152
- 0.0,
1153
- 0.0,
1154
- 1.0,
1155
- 0.0,
1156
- 0.0,
1157
- 0.0,
1158
- 0.0,
1159
- 1.0,
1160
- ]
1161
- prim = self._omni.create_dsg_group(group.name, parent[1], matrix=matrix, obj_type=obj_type)
1162
- # record the scene bounds in case they are needed later
1163
- self._groups[group.id] = [group, prim]
1164
- bounds = group.attributes.get("ENS_SCENE_BOUNDS", None)
1165
- if bounds:
1166
- minmax = []
1167
- for v in bounds.split(","):
1168
- try:
1169
- minmax.append(float(v))
1170
- except Exception:
1171
- pass
1172
- if len(minmax) == 6:
1173
- self._scene_bounds = minmax
1174
-
1175
- def handle_variable(self, var: Any) -> None:
1176
- """Handle a DSG UPDATE_VARIABLE command
1177
-
1178
- Save off the EnSight variable DSG command object.
1179
-
1180
- Parameters
1181
- ----------
1182
- var:
1183
- The command coming from the EnSight stream.
1184
- """
1185
- self._variables[var.id] = var
1186
-
1187
- def handle_view(self, view: Any) -> None:
1188
- """Handle a DSG UPDATE_VIEW command
1189
-
1190
- Map a view command into a new Omniverse stage and populate it with materials/lights.
1191
-
1192
- Parameters
1193
- ----------
1194
- view:
1195
- The command coming from the EnSight stream.
1196
- """
1197
- self._scene_bounds = None
1198
- # Create a new root stage in Omniverse
1199
- self._omni.create_new_stage()
1200
- # Create the root group/camera
1201
- camera_info = view
1202
- if self._vrmode:
1203
- camera_info = None
1204
- root = self._omni.create_dsg_root(camera=camera_info)
1205
- self._omni.checkpoint("Created base scene")
1206
- # Create a distance and dome light in the scene
1207
- # self._omni.createDistantLight()
1208
- # self._omni.createDomeLight("./Materials/kloofendal_48d_partly_cloudy.hdr")
1209
- self._omni.createDomeLight("./Materials/000_sky.exr")
1210
- self._omni.checkpoint("Added lights to stage")
1211
- # Upload a material and textures to the Omniverse server
1212
- self._omni.uploadMaterial()
1213
- self._omni.create_dsg_variable_textures(self._variables)
1214
- # record
1215
- self._groups[view.id] = [view, root]
1216
-
1217
692
 
1218
693
  if __name__ == "__main__":
1219
694
  parser = argparse.ArgumentParser(
@@ -1305,26 +780,28 @@ if __name__ == "__main__":
1305
780
  destinationPath = args.path
1306
781
  loggingEnabled = args.verbose
1307
782
 
1308
- # Make the OmniVerse connection
1309
- target = OmniverseWrapper(path=destinationPath, verbose=loggingEnabled)
1310
-
783
+ # Build the OmniVerse connection
784
+ target = OmniverseWrapper(path=destinationPath, verbose=loggingEnabled, live_edit=args.live)
1311
785
  # Print the username for the server
1312
786
  target.username()
1313
787
 
1314
788
  if loggingEnabled:
1315
- logging.info("OmniVerse connection established.")
789
+ logging.info("Omniverse connection established.")
1316
790
 
1317
- dsg_link = DSGOmniverseLink(
1318
- omni=target,
791
+ # link it to a DSG session
792
+ update_handler = OmniverseUpdateHandler(target)
793
+ dsg_link = DSGSession(
1319
794
  port=args.port,
1320
795
  host=args.host,
1321
796
  vrmode=args.vrmode,
1322
797
  security_code=args.security,
1323
798
  verbose=loggingEnabled,
1324
799
  normalize_geometry=args.normalize,
800
+ handler=update_handler,
1325
801
  )
802
+
1326
803
  if loggingEnabled:
1327
- logging.info(f"Make DSG connection to: {args.host}:{args.port}")
804
+ dsg_link.log(f"Making DSG connection to: {args.host}:{args.port}")
1328
805
 
1329
806
  # Start the DSG link
1330
807
  err = dsg_link.start()
@@ -1337,19 +814,13 @@ if __name__ == "__main__":
1337
814
  # Live operation
1338
815
  if args.live:
1339
816
  if loggingEnabled:
1340
- logging.info("Waiting for remote push operations")
817
+ dsg_link.log("Waiting for remote push operations")
1341
818
  while not dsg_link.is_shutdown():
1342
819
  dsg_link.handle_one_update()
1343
820
 
1344
821
  # Done...
1345
822
  if loggingEnabled:
1346
- logging.info("Shutting down DSG connection")
823
+ dsg_link.log("Shutting down DSG connection")
1347
824
  dsg_link.end()
1348
825
 
1349
- # Add a material to the box
1350
- # target.createMaterial(boxMesh)
1351
-
1352
- # Add a Nucleus Checkpoint to the stage
1353
- # target.checkpoint("Add material to the box")
1354
-
1355
826
  target.shutdown()