ansys-pyensight-core 0.11.0__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.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1196 @@
1
+ # Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
2
+ # Copyright 2020 NVIDIA Corporation
3
+
4
+ # contains the following notice:
5
+ #
6
+ ###############################################################################
7
+ # Copyright 2020 NVIDIA Corporation
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
10
+ # this software and associated documentation files (the "Software"), to deal in
11
+ # the Software without restriction, including without limitation the rights to
12
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
13
+ # the Software, and to permit persons to whom the Software is furnished to do so,
14
+ # subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in all
17
+ # copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
21
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
22
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
23
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+ #
26
+ ###############################################################################
27
+ import logging
28
+ import math
29
+ import os
30
+ import platform
31
+ import shutil
32
+ import sys
33
+ import tempfile
34
+ from typing import Any, Dict, List, Optional
35
+ import warnings
36
+
37
+ from ansys.pyensight.core.utils.dsg_server import Part, UpdateHandler
38
+ import numpy
39
+ import png
40
+
41
+ try:
42
+ from pxr import Gf, Kind, Sdf, Usd, UsdGeom, UsdLux, UsdShade
43
+ except ModuleNotFoundError:
44
+ if sys.version_info.minor >= 14:
45
+ warnings.warn("USD Export not supported for Python >= 3.14")
46
+ sys.exit(1)
47
+ is_linux_arm64 = platform.system() == "Linux" and platform.machine() == "aarch64"
48
+ if is_linux_arm64:
49
+ warnings.warn("USD Export not supported on Linux ARM platforms")
50
+ sys.exit(1)
51
+
52
+
53
+ class OmniverseWrapper(object):
54
+ def __init__(
55
+ self,
56
+ live_edit: bool = False,
57
+ destination: str = "",
58
+ line_width: float = 0.0,
59
+ ) -> None:
60
+ # File extension. For debugging, .usda is sometimes helpful.
61
+ self._ext = ".usd"
62
+ self._cleaned_index = 0
63
+ self._cleaned_names: dict = {}
64
+ self._connectionStatusSubscription = None
65
+ self._stage = None
66
+ self._destinationPath: str = ""
67
+ self._old_stages: list = []
68
+ self._stagename: str = "dsg_scene" + self._ext
69
+ self._live_edit: bool = live_edit
70
+ if self._live_edit:
71
+ self._stagename = "dsg_scene.live"
72
+ # USD time slider will have 120 tick marks per second of animation time
73
+ self._time_codes_per_second: float = 120.0
74
+ # Omniverse content currently only scales correctly for scenes in cm. DJB, Feb 2025
75
+ self._units_per_meter: float = 100.0
76
+ self._up_axis: str = UsdGeom.Tokens.y
77
+ if destination:
78
+ self.destination = destination
79
+
80
+ self._line_width = line_width
81
+ self._centroid: Optional[list] = None
82
+ # Record the files per timestep, per mesh type. {part_name: {"surfaces": [], "lines": [], "points": []} }
83
+ self._time_files: dict = {}
84
+
85
+ @property
86
+ def destination(self) -> str:
87
+ """The current output directory."""
88
+ return self._destinationPath
89
+
90
+ @destination.setter
91
+ def destination(self, directory: str) -> None:
92
+ self._destinationPath = directory
93
+ if not self.is_valid_destination(directory):
94
+ logging.warning(f"Invalid destination path: {directory}")
95
+
96
+ @property
97
+ def line_width(self) -> float:
98
+ return self._line_width
99
+
100
+ @line_width.setter
101
+ def line_width(self, line_width: float) -> None:
102
+ self._line_width = line_width
103
+
104
+ def shutdown(self) -> None:
105
+ """
106
+ Shutdown the connection to Omniverse cleanly.
107
+ """
108
+ self._connectionStatusSubscription = None
109
+
110
+ @staticmethod
111
+ def is_valid_destination(path: str) -> bool:
112
+ """
113
+ Verify that the target path is a writeable directory.
114
+
115
+ Parameters
116
+ ----------
117
+ path
118
+ The path to check
119
+
120
+ Returns
121
+ -------
122
+ True if the path is a writeable directory, False otherwise.
123
+ """
124
+ return os.access(path, os.W_OK)
125
+
126
+ def stage_url(self, name: Optional[str] = None) -> str:
127
+ """
128
+ For a given object name, create the URL for the item.
129
+ Parameters
130
+ ----------
131
+ name: the name of the object to generate the URL for. If None, it will be the URL for the
132
+ stage name.
133
+
134
+ Returns
135
+ -------
136
+ The URL for the object.
137
+ """
138
+ if name is None:
139
+ name = self._stagename
140
+ return os.path.join(self._destinationPath, name)
141
+
142
+ def delete_old_stages(self) -> None:
143
+ """
144
+ Remove all the stages included in the "_old_stages" list.
145
+ If a stage is in use and cannot be removed, keep its name in _old_stages
146
+ to retry later.
147
+ """
148
+ stages_unremoved = list()
149
+ while self._old_stages:
150
+ stage = self._old_stages.pop()
151
+ try:
152
+ if os.path.isfile(stage):
153
+ os.remove(stage)
154
+ else:
155
+ shutil.rmtree(stage, ignore_errors=True, onerror=None)
156
+ except OSError:
157
+ if not stage.endswith("_manifest" + self._ext):
158
+ stages_unremoved.append(stage)
159
+ self._old_stages = stages_unremoved
160
+
161
+ def create_new_stage(self) -> None:
162
+ """
163
+ Create a new stage. using the current stage name.
164
+ """
165
+ logging.info(f"Creating Omniverse stage: {self.stage_url()}")
166
+ if self._stage:
167
+ self._stage.Unload()
168
+ self._stage = None
169
+ self.delete_old_stages()
170
+ self._stage = Usd.Stage.CreateNew(self.stage_url())
171
+ # record the stage in the "_old_stages" list.
172
+ self._old_stages.append(self.stage_url())
173
+ UsdGeom.SetStageUpAxis(self._stage, self._up_axis)
174
+ UsdGeom.SetStageMetersPerUnit(self._stage, 1.0 / self._units_per_meter)
175
+ logging.info(f"Created stage: {self.stage_url()}")
176
+
177
+ def save_stage(self, comment: str = "") -> None:
178
+ """
179
+ For live connections, save the current edit and allow live processing.
180
+
181
+ Presently, live connections are disabled.
182
+ """
183
+ self._stage.GetRootLayer().Save() # type:ignore
184
+
185
+ def clear_cleaned_names(self) -> None:
186
+ """
187
+ Clear the list of cleaned names
188
+ """
189
+ self._cleaned_names = {}
190
+ self._cleaned_index = 0
191
+ # Reset the list of files per timestep
192
+ self._time_files = {}
193
+
194
+ def clean_name(self, name: str, id_name: Any = None) -> str:
195
+ """Generate a valid USD name
196
+
197
+ From a base (EnSight) varname, partname, etc. and the DSG id, generate
198
+ a unique, valid USD name. Save the names so that if the same name
199
+ comes in again, the previously computed name is returned and if the
200
+ manipulation results in a conflict, the name can be made unique.
201
+
202
+ Parameters
203
+ ----------
204
+ name:
205
+ The name to generate a USD name for.
206
+
207
+ id_name:
208
+ The DSG id associated with the DSG name, if any.
209
+
210
+ Returns
211
+ -------
212
+ A unique USD name.
213
+ """
214
+ orig_name = name
215
+ # return any previously generated name
216
+ if (name, id_name) in self._cleaned_names:
217
+ return self._cleaned_names[(name, id_name)]
218
+ # replace invalid characters. EnSight uses a number of characters that are illegal in USD names.
219
+ replacements = {
220
+ ord("+"): "_",
221
+ ord("-"): "_",
222
+ ord("."): "_",
223
+ ord(":"): "_",
224
+ ord("["): "_",
225
+ ord("]"): "_",
226
+ ord("("): "_",
227
+ ord(")"): "_",
228
+ ord("<"): "_",
229
+ ord(">"): "_",
230
+ ord("/"): "_",
231
+ ord("="): "_",
232
+ ord(","): "_",
233
+ ord(" "): "_",
234
+ ord("\\"): "_",
235
+ ord("^"): "_",
236
+ ord("!"): "_",
237
+ ord("#"): "_",
238
+ ord("%"): "_",
239
+ ord("&"): "_",
240
+ }
241
+ name = name.translate(replacements)
242
+ if name[0].isdigit():
243
+ name = f"_{name}"
244
+ if id_name is not None:
245
+ name = name + "_" + str(id_name)
246
+ if name in self._cleaned_names.values():
247
+ # Make the name unique
248
+ while f"{name}_{self._cleaned_index}" in self._cleaned_names.values():
249
+ self._cleaned_index += 1
250
+ name = f"{name}_{self._cleaned_index}"
251
+ # store off the cleaned name
252
+ self._cleaned_names[(orig_name, id_name)] = name
253
+ return name
254
+
255
+ @staticmethod
256
+ def decompose_matrix(values: Any) -> Any:
257
+ """
258
+ Decompose an array of floats (representing a 4x4 matrix) into scale, rotation and translation.
259
+ Parameters
260
+ ----------
261
+ values:
262
+ 16 values (input to Gf.Matrix4f CTOR)
263
+
264
+ Returns
265
+ -------
266
+ (scale, rotation, translation)
267
+ """
268
+ # ang_convert = 180.0/math.pi
269
+ ang_convert = 1.0
270
+ trans_convert = 1.0
271
+ m = Gf.Matrix4f(*values)
272
+ m = m.GetTranspose()
273
+
274
+ s = math.sqrt(m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2])
275
+ # cleanup scale
276
+ m = m.RemoveScaleShear()
277
+ # r = m.ExtractRotation()
278
+ R = m.ExtractRotationMatrix()
279
+ r = [
280
+ math.atan2(R[2][1], R[2][2]) * ang_convert,
281
+ math.atan2(-R[2][0], 1.0) * ang_convert,
282
+ math.atan2(R[1][0], R[0][0]) * ang_convert,
283
+ ]
284
+ t = m.ExtractTranslation()
285
+ t = [t[0] * trans_convert, t[1] * trans_convert, t[2] * trans_convert]
286
+ return s, r, t
287
+
288
+ # Common code to create the part manifest file and the file per timestep
289
+ def create_dsg_surfaces_file(
290
+ self,
291
+ file_url, # SdfPath, location on disk
292
+ part_path: str, # base path name, such as "/Root/Case_1/Isosurface_part"
293
+ verts,
294
+ normals,
295
+ conn,
296
+ tcoords,
297
+ diffuse,
298
+ variable,
299
+ mat_info,
300
+ is_manifest: bool,
301
+ ):
302
+ if is_manifest and file_url in self._old_stages:
303
+ return False
304
+ if not is_manifest and os.path.exists(file_url):
305
+ return False
306
+
307
+ stage = Usd.Stage.CreateNew(file_url)
308
+ UsdGeom.SetStageUpAxis(stage, self._up_axis)
309
+ UsdGeom.SetStageMetersPerUnit(stage, 1.0 / self._units_per_meter)
310
+ self._old_stages.append(file_url)
311
+
312
+ part_prim = stage.OverridePrim(part_path)
313
+
314
+ surfaces_prim = self.create_xform_node(stage, part_path + "/surfaces")
315
+ mesh = UsdGeom.Mesh.Define(stage, str(surfaces_prim.GetPath()) + "/Mesh")
316
+ mesh.CreateDoubleSidedAttr().Set(True)
317
+ pt_attr = mesh.CreatePointsAttr()
318
+ if verts is not None:
319
+ pt_attr.Set(verts, 0)
320
+ norm_attr = mesh.CreateNormalsAttr()
321
+ if normals is not None:
322
+ norm_attr.Set(normals, 0)
323
+ fvc_attr = mesh.CreateFaceVertexCountsAttr()
324
+ fvi_attr = mesh.CreateFaceVertexIndicesAttr()
325
+ if conn is not None:
326
+ fvc_attr.Set([3] * (conn.size // 3), 0)
327
+ fvi_attr.Set(conn, 0)
328
+
329
+ primvarsAPI = UsdGeom.PrimvarsAPI(mesh)
330
+ texCoords = primvarsAPI.CreatePrimvar(
331
+ "st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
332
+ )
333
+ texCoords.SetInterpolation("vertex")
334
+ if tcoords is not None and variable is not None:
335
+ texCoords.Set(tcoords, 0)
336
+
337
+ stage.SetDefaultPrim(part_prim)
338
+ stage.SetStartTimeCode(0)
339
+ stage.SetEndTimeCode(0)
340
+
341
+ self.create_dsg_material(
342
+ stage,
343
+ mesh,
344
+ str(surfaces_prim.GetPath()),
345
+ diffuse=diffuse,
346
+ variable=variable,
347
+ mat_info=mat_info,
348
+ )
349
+
350
+ stage.Save()
351
+ return True
352
+
353
+ def create_dsg_mesh_block(
354
+ self,
355
+ part: Part,
356
+ name,
357
+ id,
358
+ part_hash,
359
+ parent_prim,
360
+ verts,
361
+ conn,
362
+ normals,
363
+ tcoords,
364
+ matrix=[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
365
+ diffuse=[1.0, 1.0, 1.0, 1.0],
366
+ variable=None,
367
+ timeline=[0.0, 0.0],
368
+ first_timestep=False,
369
+ mat_info={},
370
+ ):
371
+ if self._stage is None:
372
+ return
373
+
374
+ # 1D texture map for variables https://graphics.pixar.com/usd/release/tut_simple_shading.html
375
+ # create the part usd object
376
+ part_base_name = self.clean_name(name)
377
+ partname = part_base_name + part_hash.hexdigest()
378
+ stage_name = "/Parts/" + partname + self._ext
379
+ part_stage_url = self.stage_url(os.path.join("Parts", partname + self._ext))
380
+
381
+ # Make the manifest file - once for all timesteps
382
+ part_manifest_url_relative = "./Parts/" + part_base_name + "_manifest" + self._ext
383
+ part_manifest_url = self.stage_url(part_manifest_url_relative)
384
+ created_file = self.create_dsg_surfaces_file(
385
+ part_manifest_url,
386
+ str(parent_prim.GetPath()),
387
+ None,
388
+ None,
389
+ None,
390
+ None,
391
+ diffuse,
392
+ variable,
393
+ mat_info,
394
+ True,
395
+ )
396
+ if created_file:
397
+ self._stage.GetRootLayer().subLayerPaths.append(part_manifest_url_relative)
398
+
399
+ # Make the per-timestep file
400
+ created_file = self.create_dsg_surfaces_file(
401
+ part_stage_url,
402
+ str(parent_prim.GetPath()),
403
+ verts,
404
+ normals,
405
+ conn,
406
+ tcoords,
407
+ diffuse,
408
+ variable,
409
+ mat_info,
410
+ False,
411
+ )
412
+
413
+ # Glue the file into the main stage
414
+ path = parent_prim.GetPath().AppendChild("surfaces")
415
+ surfaces_prim = self._stage.OverridePrim(path)
416
+ self.add_timestep_valueclip(
417
+ part_base_name,
418
+ "surfaces",
419
+ surfaces_prim,
420
+ part_manifest_url_relative,
421
+ timeline,
422
+ stage_name,
423
+ )
424
+
425
+ return part_stage_url
426
+
427
+ def get_time_files(self, part_name: str, mesh_type: str):
428
+ if part_name not in self._time_files:
429
+ self._time_files[part_name] = {"surfaces": [], "lines": [], "points": []}
430
+ return self._time_files[part_name][mesh_type]
431
+
432
+ def add_timestep_valueclip(
433
+ self,
434
+ part_name: str,
435
+ mesh_type: str,
436
+ part_prim: UsdGeom.Xform,
437
+ manifest_path: str,
438
+ timeline: List[float],
439
+ stage_name: str,
440
+ ) -> None:
441
+ clips_api = Usd.ClipsAPI(part_prim)
442
+ asset_path = "." + stage_name
443
+
444
+ time_files = self.get_time_files(part_name, mesh_type)
445
+
446
+ if len(time_files) == 0 or time_files[-1][0] != asset_path:
447
+ time_files.append((asset_path, timeline[0]))
448
+ clips_api.SetClipAssetPaths([time_file[0] for time_file in time_files])
449
+ clips_api.SetClipActive(
450
+ [
451
+ (time_file[1] * self._time_codes_per_second, ii)
452
+ for ii, time_file in enumerate(time_files)
453
+ ]
454
+ )
455
+ clips_api.SetClipTimes(
456
+ [
457
+ (time_file[1] * self._time_codes_per_second, 0)
458
+ for ii, time_file in enumerate(time_files)
459
+ ]
460
+ )
461
+ clips_api.SetClipPrimPath(str(part_prim.GetPath()))
462
+ clips_api.SetClipManifestAssetPath(Sdf.AssetPath(manifest_path))
463
+
464
+ # Common code to create the part manifest file and the file per timestep
465
+ def create_dsg_lines_file(
466
+ self,
467
+ file_url, # SdfPath, location on disk
468
+ part_path: str, # base path name, such as "/Root/Case_1/Isosurface_part"
469
+ verts,
470
+ width: float,
471
+ tcoords,
472
+ diffuse,
473
+ var_cmd,
474
+ mat_info,
475
+ is_manifest: bool,
476
+ ):
477
+ if is_manifest and file_url in self._old_stages:
478
+ return False
479
+ if not is_manifest and os.path.exists(file_url):
480
+ return False
481
+
482
+ stage = Usd.Stage.CreateNew(file_url)
483
+ UsdGeom.SetStageUpAxis(stage, self._up_axis)
484
+ UsdGeom.SetStageMetersPerUnit(stage, 1.0 / self._units_per_meter)
485
+ self._old_stages.append(file_url)
486
+
487
+ part_prim = stage.OverridePrim(part_path)
488
+
489
+ lines_prim = self.create_xform_node(stage, part_path + "/lines")
490
+
491
+ lines = UsdGeom.BasisCurves.Define(stage, str(lines_prim.GetPath()) + "/Lines")
492
+ lines.CreateDoubleSidedAttr().Set(True)
493
+ pt_attr = lines.CreatePointsAttr()
494
+ vc_attr = lines.CreateCurveVertexCountsAttr()
495
+ if verts is not None:
496
+ pt_attr.Set(verts, 0)
497
+ vc_attr.Set([2] * (verts.size // 6), 0)
498
+ lines.CreatePurposeAttr().Set("render")
499
+ lines.CreateTypeAttr().Set("linear")
500
+ lines.CreateWidthsAttr([width])
501
+ lines.SetWidthsInterpolation("constant")
502
+
503
+ # Rounded endpoint are a primvar
504
+ primvarsAPI = UsdGeom.PrimvarsAPI(lines)
505
+ endCaps = primvarsAPI.CreatePrimvar(
506
+ "endcaps", Sdf.ValueTypeNames.Int, UsdGeom.Tokens.constant
507
+ )
508
+ endCaps.Set(2) # Rounded = 2
509
+
510
+ prim = lines.GetPrim()
511
+ wireframe = width == 0.0
512
+ prim.CreateAttribute("omni:scene:visualization:drawWireframe", Sdf.ValueTypeNames.Bool).Set(
513
+ wireframe
514
+ )
515
+
516
+ if (tcoords is not None) and var_cmd:
517
+ primvarsAPI = UsdGeom.PrimvarsAPI(lines)
518
+ texCoords = primvarsAPI.CreatePrimvar(
519
+ "st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
520
+ )
521
+ if tcoords is not None and var_cmd is not None:
522
+ texCoords.Set(tcoords, 0)
523
+ texCoords.SetInterpolation("vertex")
524
+ stage.SetDefaultPrim(part_prim)
525
+ stage.SetStartTimeCode(0)
526
+ stage.SetEndTimeCode(0)
527
+
528
+ self.create_dsg_material(
529
+ stage,
530
+ lines,
531
+ str(lines_prim.GetPath()),
532
+ diffuse=diffuse,
533
+ variable=var_cmd,
534
+ mat_info=mat_info,
535
+ )
536
+ stage.Save()
537
+ return True
538
+
539
+ def create_dsg_lines(
540
+ self,
541
+ name,
542
+ id,
543
+ part_hash,
544
+ parent_prim,
545
+ verts,
546
+ tcoords,
547
+ width,
548
+ matrix=[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
549
+ diffuse=[1.0, 1.0, 1.0, 1.0],
550
+ variable=None,
551
+ timeline=[0.0, 0.0],
552
+ first_timestep=False,
553
+ mat_info={},
554
+ ):
555
+ # include the line width in the hash
556
+ part_hash.update(str(self.line_width).encode("utf-8"))
557
+
558
+ part_base_name = self.clean_name(name) + "_l"
559
+ partname = part_base_name + part_hash.hexdigest()
560
+ stage_name = "/Parts/" + partname + self._ext
561
+ part_stage_url = self.stage_url(os.path.join("Parts", partname + self._ext))
562
+
563
+ # Make the manifest file - once for all timesteps
564
+ part_manifest_url_relative = "./Parts/" + part_base_name + "_manifest" + self._ext
565
+ part_manifest_url = self.stage_url(part_manifest_url_relative)
566
+ created_file = self.create_dsg_lines_file(
567
+ part_manifest_url,
568
+ str(parent_prim.GetPath()),
569
+ None,
570
+ width,
571
+ None,
572
+ diffuse,
573
+ variable,
574
+ mat_info,
575
+ True,
576
+ )
577
+ if created_file:
578
+ self._stage.GetRootLayer().subLayerPaths.append(part_manifest_url_relative)
579
+
580
+ # Make the per-timestep file
581
+ created_file = self.create_dsg_lines_file(
582
+ part_stage_url,
583
+ str(parent_prim.GetPath()),
584
+ verts,
585
+ width,
586
+ tcoords,
587
+ diffuse,
588
+ variable,
589
+ mat_info,
590
+ False,
591
+ )
592
+
593
+ # Glue the file into the main stage
594
+ path = parent_prim.GetPath().AppendChild("lines")
595
+ lines_prim = self._stage.OverridePrim(path)
596
+ self.add_timestep_valueclip(
597
+ part_base_name, "lines", lines_prim, part_manifest_url_relative, timeline, stage_name
598
+ )
599
+
600
+ return part_stage_url
601
+
602
+ # Common code to create the part manifest file and the file per timestep
603
+ def create_dsg_points_file(
604
+ self,
605
+ file_url, # SdfPath, location on disk
606
+ part_path: str, # base path name, such as "/Root/Case_1/Isosurface_part"
607
+ verts,
608
+ sizes,
609
+ colors,
610
+ default_size: float,
611
+ default_color,
612
+ is_manifest: bool,
613
+ ):
614
+ if is_manifest and file_url in self._old_stages:
615
+ return False
616
+ if not is_manifest and os.path.exists(file_url):
617
+ return False
618
+
619
+ stage = Usd.Stage.CreateNew(file_url)
620
+ UsdGeom.SetStageUpAxis(stage, self._up_axis)
621
+ UsdGeom.SetStageMetersPerUnit(stage, 1.0 / self._units_per_meter)
622
+ self._old_stages.append(file_url)
623
+
624
+ part_prim = stage.OverridePrim(part_path)
625
+ points = UsdGeom.Points.Define(stage, part_path + "/points")
626
+ pt_attr = points.CreatePointsAttr()
627
+ w_attr = points.CreateWidthsAttr()
628
+ if verts is not None:
629
+ pt_attr.Set(verts, 0)
630
+ if sizes is not None and sizes.size == (verts.size // 3):
631
+ w_attr.Set(sizes, 0)
632
+ else:
633
+ w_attr.Set([default_size] * (verts.size // 3), 0)
634
+
635
+ colorAttr = points.GetPrim().GetAttribute("primvars:displayColor")
636
+ colorAttr.SetMetadata("interpolation", "vertex")
637
+ if verts is not None:
638
+ if colors is not None and colors.size == verts.size:
639
+ colorAttr.Set(colors, 0)
640
+ else:
641
+ colorAttr.Set([default_color[0:3]] * (verts.size // 3), 0)
642
+
643
+ stage.SetDefaultPrim(part_prim)
644
+ stage.SetStartTimeCode(0)
645
+ stage.SetEndTimeCode(0)
646
+
647
+ stage.Save()
648
+ return True
649
+
650
+ def create_dsg_points(
651
+ self,
652
+ name,
653
+ id,
654
+ part_hash,
655
+ parent_prim,
656
+ verts,
657
+ sizes,
658
+ colors,
659
+ matrix=[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
660
+ default_size=1.0,
661
+ default_color=[1.0, 1.0, 1.0, 1.0],
662
+ timeline=[0.0, 0.0],
663
+ first_timestep=False,
664
+ ):
665
+ part_base_name = self.clean_name(name) + "_p"
666
+ partname = part_base_name + part_hash.hexdigest()
667
+ stage_name = "/Parts/" + partname + self._ext
668
+ part_stage_url = self.stage_url(os.path.join("Parts", partname + self._ext))
669
+
670
+ # Make the manifest file - once for all timesteps
671
+ part_manifest_url_relative = "./Parts/" + part_base_name + "_manifest" + self._ext
672
+ part_manifest_url = self.stage_url(part_manifest_url_relative)
673
+ created_file = self.create_dsg_points_file(
674
+ part_manifest_url,
675
+ str(parent_prim.GetPath()),
676
+ None,
677
+ None,
678
+ None,
679
+ default_size,
680
+ default_color,
681
+ True,
682
+ )
683
+ if created_file:
684
+ self._stage.GetRootLayer().subLayerPaths.append(part_manifest_url_relative)
685
+
686
+ # Make the per-timestep file
687
+ created_file = self.create_dsg_points_file(
688
+ part_stage_url,
689
+ str(parent_prim.GetPath()),
690
+ verts,
691
+ sizes,
692
+ colors,
693
+ default_size,
694
+ default_color,
695
+ False,
696
+ )
697
+
698
+ # Glue the file into the main stage
699
+ path = parent_prim.GetPath().AppendChild("points")
700
+ points_prim = self._stage.OverridePrim(path)
701
+ self.add_timestep_valueclip(
702
+ part_base_name, "points", points_prim, part_manifest_url_relative, timeline, stage_name
703
+ )
704
+
705
+ return part_stage_url
706
+
707
+ def create_dsg_material(
708
+ self, stage, mesh, root_name, diffuse=[1.0, 1.0, 1.0, 1.0], variable=None, mat_info={}
709
+ ):
710
+ # https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html
711
+ # Use ior==1.0 to be more like EnSight - rays of light do not bend when passing through transparent objs
712
+ material = UsdShade.Material.Define(stage, root_name + "/Material")
713
+ pbrShader = UsdShade.Shader.Define(stage, root_name + "/Material/PBRShader")
714
+ pbrShader.CreateIdAttr("UsdPreviewSurface")
715
+ smoothness = mat_info.get("smoothness", 0.0)
716
+ pbrShader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(1.0 - smoothness)
717
+ metallic = mat_info.get("metallic", 0.0)
718
+ pbrShader.CreateInput("metallic", Sdf.ValueTypeNames.Float).Set(metallic)
719
+ opacity = mat_info.get("opacity", diffuse[3])
720
+ pbrShader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(opacity)
721
+ pbrShader.CreateInput("ior", Sdf.ValueTypeNames.Float).Set(1.0)
722
+ pbrShader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
723
+ if variable:
724
+ stReader = UsdShade.Shader.Define(stage, root_name + "/Material/stReader")
725
+ stReader.CreateIdAttr("UsdPrimvarReader_float2")
726
+ diffuseTextureSampler = UsdShade.Shader.Define(
727
+ stage, root_name + "/Material/diffuseTexture"
728
+ )
729
+ diffuseTextureSampler.CreateIdAttr("UsdUVTexture")
730
+ name = self.clean_name(variable.name)
731
+ filename = f"./Textures/palette_{name}.png"
732
+ diffuseTextureSampler.CreateInput("file", Sdf.ValueTypeNames.Asset).Set(filename)
733
+ diffuseTextureSampler.CreateInput("st", Sdf.ValueTypeNames.Float2).ConnectToSource(
734
+ stReader.ConnectableAPI(), "result"
735
+ )
736
+ diffuseTextureSampler.CreateOutput("rgb", Sdf.ValueTypeNames.Float3)
737
+ pbrShader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).ConnectToSource(
738
+ diffuseTextureSampler.ConnectableAPI(), "rgb"
739
+ )
740
+ stInput = material.CreateInput("frame:stPrimvarName", Sdf.ValueTypeNames.Token)
741
+ stInput.Set("st")
742
+ stReader.CreateInput("varname", Sdf.ValueTypeNames.Token).ConnectToSource(stInput)
743
+ else:
744
+ # The colors are a mixture of content from the DSG PART protocol buffer
745
+ # and the JSON material block from the material_name field.
746
+ kd = 1.0
747
+ diffuse_color = [diffuse[0], diffuse[1], diffuse[2]]
748
+ ke = 1.0
749
+ emissive_color = [0.0, 0.0, 0.0]
750
+ ks = 1.0
751
+ specular_color = [0.0, 0.0, 0.0]
752
+ mat_name = mat_info.get("name", "")
753
+ if mat_name.startswith("ensight"):
754
+ diffuse_color = mat_info.get("diffuse", diffuse_color)
755
+ if mat_name != "ensight/Default":
756
+ ke = mat_info.get("ke", ke)
757
+ emissive_color = mat_info.get("emissive", emissive_color)
758
+ ks = mat_info.get("ks", ks)
759
+ specular_color = mat_info.get("specular", specular_color)
760
+ # Set the colors
761
+ color = Gf.Vec3f(diffuse_color[0] * kd, diffuse_color[1] * kd, diffuse_color[2] * kd)
762
+ pbrShader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(color)
763
+ color = Gf.Vec3f(emissive_color[0] * ke, emissive_color[1] * ke, emissive_color[2] * ke)
764
+ pbrShader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(color)
765
+ color = Gf.Vec3f(specular_color[0] * ks, specular_color[1] * ks, specular_color[2] * ks)
766
+ pbrShader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(color)
767
+
768
+ material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface")
769
+ mat_binding_api = UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim())
770
+ mat_binding_api.Bind(material)
771
+
772
+ return material
773
+
774
+ def create_dsg_variable_textures(self, variables):
775
+ with tempfile.TemporaryDirectory() as tempdir:
776
+ # make folder: {tempdir}/scratch/Textures/{palette_*.png}
777
+ os.makedirs(f"{tempdir}/scratch/Textures", exist_ok=True)
778
+ for var in variables.values():
779
+ data = bytearray(var.texture)
780
+ n_pixels = int(len(data) / 4)
781
+ row = []
782
+ for i in range(n_pixels):
783
+ row.append(data[i * 4 + 0])
784
+ row.append(data[i * 4 + 1])
785
+ row.append(data[i * 4 + 2])
786
+ io = png.Writer(width=n_pixels, height=2, bitdepth=8, greyscale=False)
787
+ rows = [row, row]
788
+ name = self.clean_name(var.name)
789
+ with open(f"{tempdir}/scratch/Textures/palette_{name}.png", "wb") as fp:
790
+ io.write(fp, rows)
791
+ uriPath = self._destinationPath + "/Parts/Textures"
792
+ shutil.rmtree(uriPath, ignore_errors=True, onerror=None)
793
+ shutil.copytree(f"{tempdir}/scratch/Textures", uriPath)
794
+
795
+ def create_xform_node(self, stage, name):
796
+ xform_node = UsdGeom.Xform.Define(stage, name)
797
+ xform_node.AddTranslateOp().Set((0, 0, 0))
798
+ xform_node.AddRotateXYZOp().Set((0, 0, 0))
799
+ xform_node.AddScaleOp().Set((1, 1, 1))
800
+ if self._centroid is not None:
801
+ xform_api = UsdGeom.XformCommonAPI(xform_node.GetPrim())
802
+ xform_api.SetPivot(Gf.Vec3f(self._centroid) * self._units_per_meter)
803
+ return xform_node
804
+
805
+ def create_dsg_root(self):
806
+ root_name = "/Root"
807
+ self.create_xform_node(self._stage, root_name)
808
+
809
+ # Define the defaultPrim as the /Root prim
810
+ root_prim = self._stage.GetPrimAtPath(root_name)
811
+ self._stage.SetDefaultPrim(root_prim)
812
+ return root_prim
813
+
814
+ def update_camera(self, camera):
815
+ if camera is not None:
816
+ cam_name = "/Root/Cam"
817
+ cam_prim = UsdGeom.Xform.Define(self._stage, cam_name)
818
+ s = self._units_per_meter
819
+ cam_pos = Gf.Vec3d(camera.lookfrom[0], camera.lookfrom[1], camera.lookfrom[2]) * s
820
+ target_pos = Gf.Vec3d(camera.lookat[0], camera.lookat[1], camera.lookat[2]) * s
821
+
822
+ up_vec = Gf.Vec3d(camera.upvector[0], camera.upvector[1], camera.upvector[2])
823
+ cam_prim = self._stage.GetPrimAtPath(cam_name)
824
+ geom_cam = UsdGeom.Camera(cam_prim)
825
+ if not geom_cam:
826
+ geom_cam = UsdGeom.Camera.Define(self._stage, cam_name)
827
+ # Set camera values
828
+ # center of interest attribute unique for Kit defines the pivot for tumbling the camera
829
+ # Set as an attribute on the prim
830
+ coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
831
+ if not coi_attr.IsValid():
832
+ coi_attr = cam_prim.CreateAttribute(
833
+ "omni:kit:centerOfInterest", Sdf.ValueTypeNames.Vector3d
834
+ )
835
+ coi_attr.Set(target_pos)
836
+ # get the camera
837
+ cam = geom_cam.GetCamera()
838
+ # LOL, not sure why is might be correct, but so far it seems to work???
839
+ cam.focalLength = camera.fieldofview
840
+ dist = (target_pos - cam_pos).GetLength()
841
+ cam.clippingRange = Gf.Range1f(0.1 * dist, 1000.0 * dist)
842
+ look_at = Gf.Matrix4d()
843
+ look_at.SetLookAt(cam_pos, target_pos, up_vec)
844
+ trans_row = look_at.GetRow(3)
845
+ trans_row = Gf.Vec4d(-trans_row[0], -trans_row[1], -trans_row[2], trans_row[3])
846
+ look_at.SetRow(3, trans_row)
847
+ cam.transform = look_at
848
+
849
+ # set the updated camera
850
+ geom_cam.SetFromCamera(cam)
851
+
852
+ def create_dsg_group(
853
+ self,
854
+ name: str,
855
+ parent_prim,
856
+ obj_type: Any = None,
857
+ matrix: List[float] = [
858
+ 1.0,
859
+ 0.0,
860
+ 0.0,
861
+ 0.0,
862
+ 0.0,
863
+ 1.0,
864
+ 0.0,
865
+ 0.0,
866
+ 0.0,
867
+ 0.0,
868
+ 1.0,
869
+ 0.0,
870
+ 0.0,
871
+ 0.0,
872
+ 0.0,
873
+ 1.0,
874
+ ],
875
+ ):
876
+ path = parent_prim.GetPath().AppendChild(self.clean_name(name))
877
+ group_prim = UsdGeom.Xform.Get(self._stage, path)
878
+ if not group_prim:
879
+ group_prim = self.create_xform_node(self._stage, path)
880
+
881
+ # Map kinds
882
+ kind = Kind.Tokens.group
883
+ if obj_type == "ENS_CASE":
884
+ kind = Kind.Tokens.assembly
885
+ elif obj_type == "ENS_PART":
886
+ kind = Kind.Tokens.component
887
+ Usd.ModelAPI(group_prim).SetKind(kind)
888
+ group_prim.GetPrim().SetDisplayName(name)
889
+ logging.info(f"Created group:'{name}' {str(obj_type)}")
890
+
891
+ return group_prim
892
+
893
+ def uploadMaterial(self):
894
+ uriPath = self._destinationPath + "/Materials"
895
+ shutil.rmtree(uriPath, ignore_errors=True, onerror=None)
896
+ fullpath = os.path.join(os.path.dirname(__file__), "resources", "Materials")
897
+ shutil.copytree(fullpath, uriPath)
898
+
899
+ # Create a dome light in the scene.
900
+ def createDomeLight(self, texturePath):
901
+ newLight = UsdLux.DomeLight.Define(self._stage, "/Root/DomeLight")
902
+ newLight.CreateIntensityAttr(2200.0)
903
+ newLight.CreateTextureFileAttr(texturePath)
904
+ newLight.CreateTextureFormatAttr("latlong")
905
+
906
+ # Set rotation on domelight
907
+ xForm = newLight
908
+ rotateOp = xForm.AddXformOp(UsdGeom.XformOp.TypeRotateZYX, UsdGeom.XformOp.PrecisionFloat)
909
+ rotateOp.Set(Gf.Vec3f(270, 0, 0))
910
+
911
+
912
+ class OmniverseUpdateHandler(UpdateHandler):
913
+ """
914
+ Implement the Omniverse glue to a DSGSession instance
915
+ """
916
+
917
+ def __init__(self, omni: OmniverseWrapper):
918
+ super().__init__()
919
+ self._omni = omni
920
+ self._group_prims: Dict[int, Any] = dict()
921
+ self._root_prim = None
922
+ self._sent_textures = False
923
+ self._case_xform_applied_to_camera = False
924
+
925
+ def add_group(self, id: int, view: bool = False) -> None:
926
+ super().add_group(id, view)
927
+ group = self.session.groups[id]
928
+
929
+ if not view:
930
+ # Capture changes in line/sphere sizes if it was not set from cli
931
+ width = self.get_dsg_cmd_attribute(group, "ANSYS_linewidth")
932
+ if width:
933
+ try:
934
+ self._omni.line_width = float(width)
935
+ except ValueError:
936
+ pass
937
+
938
+ parent_prim = self._group_prims[group.parent_id]
939
+ # get the EnSight object type and the transform matrix
940
+ obj_type = self.get_dsg_cmd_attribute(group, "ENS_OBJ_TYPE")
941
+ matrix = group.matrix4x4
942
+ # Is this a "case" group (it will contain part of the camera view in the matrix)
943
+ if obj_type == "ENS_CASE":
944
+ if self.session.scene_bounds is not None:
945
+ midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
946
+ midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
947
+ midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
948
+ self._omni._centroid = [midx, midy, midz]
949
+
950
+ if not self.session.vrmode and not self._case_xform_applied_to_camera:
951
+ # if in camera mode, we need to update the camera matrix so we can
952
+ # use the identity matrix on this group. The camera should have been
953
+ # created in the "view" handler
954
+ self._case_xform_applied_to_camera = True
955
+ cam_name = "/Root/Cam"
956
+ cam_prim = self._omni._stage.GetPrimAtPath(cam_name) # type: ignore
957
+ geom_cam = UsdGeom.Camera(cam_prim)
958
+ # get the camera
959
+ cam = geom_cam.GetCamera()
960
+ c = cam.transform
961
+ m = Gf.Matrix4d(*matrix).GetTranspose()
962
+ s = self._omni._units_per_meter
963
+ trans = m.GetRow(3)
964
+ trans = Gf.Vec4d(trans[0] * s, trans[1] * s, trans[2] * s, trans[3])
965
+ m.SetRow(3, trans)
966
+ # move the model transform to the camera transform
967
+ cam.transform = c * m.GetInverse()
968
+
969
+ # Determine if the camera is principally more Y, or Z up. X up not supported.
970
+ # Omniverse' built in navigator tries to keep this direction up
971
+ # If the view is principally -Y, there is no good choice. +Y is least bad.
972
+ cam_upvec = Gf.Vec4d(0, 1, 0, 0) * cam.transform
973
+ if abs(cam_upvec[1]) >= abs(cam_upvec[2]):
974
+ self._up_axis = UsdGeom.Tokens.y
975
+ else:
976
+ self._up_axis = UsdGeom.Tokens.z
977
+ UsdGeom.SetStageUpAxis(self._omni._stage, self._up_axis)
978
+
979
+ # set the updated camera
980
+ geom_cam.SetFromCamera(cam)
981
+ # apply the inverse cam transform to move the center of interest
982
+ # from data space to camera space
983
+ coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
984
+ if coi_attr.IsValid():
985
+ coi_data = coi_attr.Get()
986
+ coi_cam = (
987
+ Gf.Vec4d(coi_data[0], coi_data[1], coi_data[2], 1.0)
988
+ * cam.transform.GetInverse()
989
+ )
990
+ coi_attr.Set(
991
+ Gf.Vec3d(
992
+ 0,
993
+ 0,
994
+ coi_cam[2] / coi_cam[3],
995
+ )
996
+ )
997
+ # use the camera view by default
998
+ self._omni._stage.GetRootLayer().customLayerData = { # type: ignore
999
+ "cameraSettings": {"boundCamera": "/Root/Cam"}
1000
+ }
1001
+ matrix = [
1002
+ 1.0,
1003
+ 0.0,
1004
+ 0.0,
1005
+ 0.0,
1006
+ 0.0,
1007
+ 1.0,
1008
+ 0.0,
1009
+ 0.0,
1010
+ 0.0,
1011
+ 0.0,
1012
+ 1.0,
1013
+ 0.0,
1014
+ 0.0,
1015
+ 0.0,
1016
+ 0.0,
1017
+ 1.0,
1018
+ ]
1019
+ prim = self._omni.create_dsg_group(
1020
+ group.name, parent_prim, matrix=matrix, obj_type=obj_type
1021
+ )
1022
+ self._group_prims[id] = prim
1023
+ else:
1024
+ # Map a view command into a new Omniverse stage and populate it with materials/lights.
1025
+ # Create a new root stage in Omniverse
1026
+
1027
+ # Create or update the root group/camera
1028
+ if not self.session.vrmode and not self._case_xform_applied_to_camera:
1029
+ self._omni.update_camera(camera=group)
1030
+
1031
+ # record
1032
+ self._group_prims[id] = self._root_prim
1033
+
1034
+ if self._omni._stage is not None:
1035
+ self._omni._stage.SetStartTimeCode(
1036
+ self.session.time_limits[0] * self._omni._time_codes_per_second
1037
+ )
1038
+ self._omni._stage.SetEndTimeCode(
1039
+ self.session.time_limits[1] * self._omni._time_codes_per_second
1040
+ )
1041
+ self._omni._stage.SetTimeCodesPerSecond(self._omni._time_codes_per_second)
1042
+
1043
+ # Send the variable textures. Safe to do so once the first view is processed.
1044
+ if not self._sent_textures:
1045
+ self._omni.create_dsg_variable_textures(self.session.variables)
1046
+ self._sent_textures = True
1047
+
1048
+ def add_variable(self, id: int) -> None:
1049
+ super().add_variable(id)
1050
+
1051
+ def finalize_part(self, part: Part) -> None:
1052
+ # generate an Omniverse compliant mesh from the Part
1053
+ if part is None or part.cmd is None:
1054
+ return
1055
+ parent_prim = self._group_prims[part.cmd.parent_id]
1056
+ obj_id = self.session.mesh_block_count
1057
+ matrix = part.cmd.matrix4x4
1058
+ name = part.cmd.name
1059
+ color = [
1060
+ part.cmd.fill_color[0] * part.cmd.diffuse,
1061
+ part.cmd.fill_color[1] * part.cmd.diffuse,
1062
+ part.cmd.fill_color[2] * part.cmd.diffuse,
1063
+ part.cmd.fill_color[3],
1064
+ ]
1065
+
1066
+ mat_info = part.material()
1067
+ if part.cmd.render == part.cmd.CONNECTIVITY:
1068
+ has_triangles = False
1069
+ command, verts, conn, normals, tcoords, var_cmd = part.nodal_surface_rep()
1070
+ if verts is not None:
1071
+ verts = numpy.multiply(verts, self._omni._units_per_meter)
1072
+ if command is not None:
1073
+ has_triangles = True
1074
+ # Generate the mesh block
1075
+ _ = self._omni.create_dsg_mesh_block(
1076
+ part,
1077
+ name,
1078
+ obj_id,
1079
+ part.hash,
1080
+ parent_prim,
1081
+ verts,
1082
+ conn,
1083
+ normals,
1084
+ tcoords,
1085
+ matrix=matrix,
1086
+ diffuse=color,
1087
+ variable=var_cmd,
1088
+ timeline=self.session.cur_timeline,
1089
+ first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
1090
+ mat_info=mat_info,
1091
+ )
1092
+ command, verts, tcoords, var_cmd = part.line_rep()
1093
+ if verts is not None:
1094
+ verts = numpy.multiply(verts, self._omni._units_per_meter)
1095
+ if command is not None:
1096
+ # If there are no triangle (ideally if these are not hidden line
1097
+ # edges), then use the base color for the part. If there are
1098
+ # triangles, then assume these are hidden line edges and use the
1099
+ # line_color.
1100
+ line_color = color
1101
+ if has_triangles:
1102
+ line_color = [
1103
+ part.cmd.line_color[0] * part.cmd.diffuse,
1104
+ part.cmd.line_color[1] * part.cmd.diffuse,
1105
+ part.cmd.line_color[2] * part.cmd.diffuse,
1106
+ part.cmd.line_color[3],
1107
+ ]
1108
+ # TODO: texture coordinates on lines are currently invalid in Omniverse
1109
+ var_cmd = None
1110
+ tcoords = None
1111
+ # line info can come from self or our parent group
1112
+ width = self._omni.line_width
1113
+ # Allow the group to override
1114
+ group = self.session.find_group_pb(part.cmd.parent_id)
1115
+ if group:
1116
+ try:
1117
+ width = float(group.attributes.get("ANSYS_linewidth", str(width)))
1118
+ except ValueError:
1119
+ pass
1120
+ if width < 0.0:
1121
+ tmp = verts.reshape(-1, 3)
1122
+ mins = numpy.min(tmp, axis=0)
1123
+ maxs = numpy.max(tmp, axis=0)
1124
+ dx = maxs[0] - mins[0]
1125
+ dy = maxs[1] - mins[1]
1126
+ dz = maxs[2] - mins[2]
1127
+ diagonal = math.sqrt(dx * dx + dy * dy + dz * dz)
1128
+ width = diagonal * math.fabs(width) / self._omni._units_per_meter
1129
+ if self._omni.line_width < 0.0:
1130
+ self._omni.line_width = width
1131
+ width = width * self._omni._units_per_meter
1132
+ # Generate the lines
1133
+ _ = self._omni.create_dsg_lines(
1134
+ name,
1135
+ obj_id,
1136
+ part.hash,
1137
+ parent_prim,
1138
+ verts,
1139
+ tcoords,
1140
+ width,
1141
+ matrix=matrix,
1142
+ diffuse=line_color,
1143
+ variable=var_cmd,
1144
+ timeline=self.session.cur_timeline,
1145
+ first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
1146
+ )
1147
+
1148
+ elif part.cmd.render == part.cmd.NODES:
1149
+ command, verts, sizes, colors, var_cmd = part.point_rep()
1150
+ if verts is not None:
1151
+ verts = numpy.multiply(verts, self._omni._units_per_meter)
1152
+ if sizes is not None:
1153
+ sizes = numpy.multiply(sizes, self._omni._units_per_meter)
1154
+ if command is not None:
1155
+ _ = self._omni.create_dsg_points(
1156
+ name,
1157
+ obj_id,
1158
+ part.hash,
1159
+ parent_prim,
1160
+ verts,
1161
+ sizes,
1162
+ colors,
1163
+ matrix=matrix,
1164
+ default_size=part.cmd.node_size_default * self._omni._units_per_meter,
1165
+ default_color=color,
1166
+ timeline=self.session.cur_timeline,
1167
+ first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
1168
+ )
1169
+ super().finalize_part(part)
1170
+
1171
+ def start_connection(self) -> None:
1172
+ super().start_connection()
1173
+
1174
+ def end_connection(self) -> None:
1175
+ super().end_connection()
1176
+
1177
+ def begin_update(self) -> None:
1178
+ super().begin_update()
1179
+ # restart the name tables
1180
+ self._omni.clear_cleaned_names()
1181
+ # clear the group Omni prims list
1182
+ self._group_prims = dict()
1183
+ self._case_xform_applied_to_camera = False
1184
+
1185
+ self._omni.create_new_stage()
1186
+ self._root_prim = self._omni.create_dsg_root()
1187
+ # Create a distance and dome light in the scene
1188
+ self._omni.createDomeLight("./Materials/000_sky.exr")
1189
+ # Upload a material to the Omniverse server
1190
+ self._omni.uploadMaterial()
1191
+ self._sent_textures = False
1192
+
1193
+ def end_update(self) -> None:
1194
+ super().end_update()
1195
+ # Stage update complete
1196
+ self._omni.save_stage()