ansys-pyensight-core 0.9.1__py3-none-any.whl → 0.9.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ansys-pyensight-core might be problematic. Click here for more details.
- ansys/pyensight/core/ensight_grpc.py +302 -0
- ansys/pyensight/core/utils/omniverse.py +203 -77
- ansys/pyensight/core/utils/omniverse_cli.py +19 -11
- ansys/pyensight/core/utils/omniverse_dsg_server.py +108 -44
- ansys/pyensight/core/utils/omniverse_glb_server.py +25 -8
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.2.dist-info}/METADATA +1 -1
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.2.dist-info}/RECORD +9 -9
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.2.dist-info}/LICENSE +0 -0
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.2.dist-info}/WHEEL +0 -0
|
@@ -4,6 +4,11 @@ This package defines the EnSightGRPC class which provides a simpler
|
|
|
4
4
|
interface to the EnSight gRPC interface, including event streams.
|
|
5
5
|
|
|
6
6
|
"""
|
|
7
|
+
from concurrent import futures
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
7
12
|
import threading
|
|
8
13
|
from typing import Any, Callable, List, Optional, Tuple, Union
|
|
9
14
|
import uuid
|
|
@@ -47,6 +52,14 @@ class EnSightGRPC(object):
|
|
|
47
52
|
# Callback for events (self._events not used)
|
|
48
53
|
self._event_callback: Optional[Callable] = None
|
|
49
54
|
self._prefix: Optional[str] = None
|
|
55
|
+
self._shmem_module = None
|
|
56
|
+
self._shmem_filename: Optional[str] = None
|
|
57
|
+
self._shmem_client = None
|
|
58
|
+
self._image_stream = None
|
|
59
|
+
self._image_thread = None
|
|
60
|
+
self._image = None
|
|
61
|
+
self._image_number = 0
|
|
62
|
+
self._sub_service = None
|
|
50
63
|
|
|
51
64
|
@property
|
|
52
65
|
def host(self) -> str:
|
|
@@ -120,6 +133,12 @@ class EnSightGRPC(object):
|
|
|
120
133
|
if self._channel:
|
|
121
134
|
self._channel.close()
|
|
122
135
|
self._channel = None
|
|
136
|
+
if self._shmem_client:
|
|
137
|
+
if self._shmem_module:
|
|
138
|
+
self._shmem_module.stream_destroy(self._shmem_client)
|
|
139
|
+
else:
|
|
140
|
+
self.command("ensight_grpc_shmem.stream_destroy(enscl._shmem_client)")
|
|
141
|
+
self._shmem_client = None
|
|
123
142
|
|
|
124
143
|
def is_connected(self) -> bool:
|
|
125
144
|
"""Check to see if the gRPC connection is live
|
|
@@ -430,3 +449,286 @@ class EnSightGRPC(object):
|
|
|
430
449
|
# signal that the gRPC connection has broken
|
|
431
450
|
self._event_stream = None
|
|
432
451
|
self._event_thread = None
|
|
452
|
+
|
|
453
|
+
def _attempt_shared_mem_import(self):
|
|
454
|
+
try:
|
|
455
|
+
import ensight_grpc_shmem
|
|
456
|
+
|
|
457
|
+
self._shmem_module = ensight_grpc_shmem
|
|
458
|
+
except ModuleNotFoundError:
|
|
459
|
+
try:
|
|
460
|
+
self.command("import enve", do_eval=False)
|
|
461
|
+
cei_home = eval(self.command("enve.home()"))
|
|
462
|
+
self.command("import ceiversion", do_eval=False)
|
|
463
|
+
cei_version = eval(self.command("ceiversion.version_suffix"))
|
|
464
|
+
self.command("import sys", do_eval=False)
|
|
465
|
+
py_version = eval(self.command("sys.version_info[:3]"))
|
|
466
|
+
is_win = True if "Win" in platform.system() else False
|
|
467
|
+
plat = "win64" if is_win else "linux_2.6_64"
|
|
468
|
+
_lib = "DLLs" if is_win else f"lib/python{py_version[0]}.{py_version[1]}"
|
|
469
|
+
dll_loc = os.path.join(
|
|
470
|
+
cei_home,
|
|
471
|
+
f"apex{cei_version}",
|
|
472
|
+
"machines",
|
|
473
|
+
plat,
|
|
474
|
+
f"Python-{py_version[0]}.{py_version[1]}.{py_version[2]}",
|
|
475
|
+
_lib,
|
|
476
|
+
)
|
|
477
|
+
if os.path.exists(dll_loc):
|
|
478
|
+
sys.path.append(dll_loc)
|
|
479
|
+
import ensight_grpc_shmem
|
|
480
|
+
|
|
481
|
+
self._shmem_module = ensight_grpc_shmem
|
|
482
|
+
except ModuleNotFoundError:
|
|
483
|
+
pass
|
|
484
|
+
|
|
485
|
+
@classmethod
|
|
486
|
+
def _find_filename(cls, size=1024 * 1024 * 25):
|
|
487
|
+
"""Create a file on disk to support shared memory transport.
|
|
488
|
+
|
|
489
|
+
A file, 25MB in size, will be created using the pid of the current
|
|
490
|
+
process to generate the filename. It will be located in a temporary
|
|
491
|
+
directory.
|
|
492
|
+
"""
|
|
493
|
+
tempdir = tempfile.mkdtemp(prefix="pyensight_shmem")
|
|
494
|
+
for i in range(100):
|
|
495
|
+
filename = os.path.join(tempdir, "shmem_{}.bin".format(os.getpid() + i))
|
|
496
|
+
if not os.path.exists(filename):
|
|
497
|
+
try:
|
|
498
|
+
tmp = open(filename, "wb")
|
|
499
|
+
tmp.write(b"\0" * size) # 25MB
|
|
500
|
+
tmp.close()
|
|
501
|
+
return filename
|
|
502
|
+
except Exception:
|
|
503
|
+
pass
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
def get_image(self):
|
|
507
|
+
"""Retrieve the current EnSight image.
|
|
508
|
+
|
|
509
|
+
When any of the image streaming systems is enabled, Python threads will receive the
|
|
510
|
+
most recent image and store them in this instance. The frame stored in this instance
|
|
511
|
+
can be accessed by calling this method
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
(tuple):
|
|
516
|
+
A tuple containing a dictionary defining the image binary
|
|
517
|
+
(pixels=bytearray, width=w, height=h) and the image frame number.
|
|
518
|
+
"""
|
|
519
|
+
return self._image, self._image_number
|
|
520
|
+
|
|
521
|
+
def _start_sub_service(self):
|
|
522
|
+
"""Start a gRPC client service.
|
|
523
|
+
When the client calls one subscribe_events() or subscribe_images() with the
|
|
524
|
+
connection set to GRPC, the interface requires the client to start a gRPC server
|
|
525
|
+
that EnSight will call back to with event/image messages. This method starts
|
|
526
|
+
such a gRPC server."""
|
|
527
|
+
try:
|
|
528
|
+
if self._sub_service is not None:
|
|
529
|
+
return
|
|
530
|
+
self._sub_service = _EnSightSubServicer(parent=self)
|
|
531
|
+
self._sub_service.start()
|
|
532
|
+
except Exception:
|
|
533
|
+
self._sub_service = None
|
|
534
|
+
|
|
535
|
+
def subscribe_images(self, flip_vertical=False, use_shmem=True):
|
|
536
|
+
"""Subscribe to an image stream.
|
|
537
|
+
|
|
538
|
+
This methond makes a EnSightService::SubscribeImages() gRPC call. If
|
|
539
|
+
use_shmem is False, the transport system will be made over gRPC. It causes
|
|
540
|
+
EnSight to make a reverse gRPC connection over with gRPC calls with the
|
|
541
|
+
various images will be made. If use_shmem is True (the default), the \ref shmem will be used.
|
|
542
|
+
|
|
543
|
+
Parameters
|
|
544
|
+
---------
|
|
545
|
+
flip_vertical: bool
|
|
546
|
+
If True, the image pixels will be flipped over the X axis
|
|
547
|
+
use_shmem: bool
|
|
548
|
+
If True, use the shared memory transport, otherwise use reverse gRPC"""
|
|
549
|
+
self.connect()
|
|
550
|
+
if use_shmem:
|
|
551
|
+
try:
|
|
552
|
+
# we need a shared memory file
|
|
553
|
+
self._shmem_filename = self._find_filename()
|
|
554
|
+
if self._shmem_filename is not None:
|
|
555
|
+
conn_type = ensight_pb2.SubscribeImageOptions.SHARED_MEM
|
|
556
|
+
options = dict(filename=self._shmem_filename)
|
|
557
|
+
image_options = ensight_pb2.SubscribeImageOptions(
|
|
558
|
+
prefix=self.prefix(),
|
|
559
|
+
type=conn_type,
|
|
560
|
+
options=options,
|
|
561
|
+
flip_vertical=flip_vertical,
|
|
562
|
+
chunk=False,
|
|
563
|
+
)
|
|
564
|
+
_ = self._stub.SubscribeImages(image_options, metadata=self._metadata())
|
|
565
|
+
# start the local server
|
|
566
|
+
if not self._shmem_module:
|
|
567
|
+
self._attempt_shared_mem_import()
|
|
568
|
+
if self._shmem_module:
|
|
569
|
+
self._shmem_client = self._shmem_module.stream_create(self._shmem_filename)
|
|
570
|
+
else:
|
|
571
|
+
self.command("import ensight_grpc_shmem", do_eval=False)
|
|
572
|
+
to_send = self._shmem_filename.replace("\\", "\\\\")
|
|
573
|
+
self.command(
|
|
574
|
+
f"enscl._shmem_client = ensight_grpc_shmem.stream_create('{to_send}')",
|
|
575
|
+
do_eval=False,
|
|
576
|
+
)
|
|
577
|
+
if self.command("enscl._shmem_client is not None"):
|
|
578
|
+
self._shmem_client = True
|
|
579
|
+
|
|
580
|
+
# turn on the polling thread
|
|
581
|
+
self._image_thread = threading.Thread(target=self._poll_images)
|
|
582
|
+
self._image_thread.daemon = True
|
|
583
|
+
self._image_thread.start()
|
|
584
|
+
return
|
|
585
|
+
except Exception as e:
|
|
586
|
+
print("Unable to subscribe to an image stream via shared memory: {}".format(str(e)))
|
|
587
|
+
|
|
588
|
+
self._start_sub_service()
|
|
589
|
+
conn_type = ensight_pb2.SubscribeImageOptions.GRPC
|
|
590
|
+
options = {}
|
|
591
|
+
if self._sub_service:
|
|
592
|
+
options = dict(uri=self._sub_service._uri)
|
|
593
|
+
image_options = ensight_pb2.SubscribeImageOptions(
|
|
594
|
+
prefix=self.prefix(),
|
|
595
|
+
type=conn_type,
|
|
596
|
+
options=options,
|
|
597
|
+
flip_vertical=flip_vertical,
|
|
598
|
+
chunk=True,
|
|
599
|
+
)
|
|
600
|
+
_ = self._stub.SubscribeImages(image_options, metadata=self._metadata())
|
|
601
|
+
|
|
602
|
+
def image_stream_enable(self, flip_vertical=False):
|
|
603
|
+
"""Enable a simple gRPC-based image stream from EnSight.
|
|
604
|
+
|
|
605
|
+
This method makes a EnSightService::GetImageStream() gRPC call into EnSight, returning
|
|
606
|
+
an ensightservice::ImageReply stream. The method creates a thread to hold this
|
|
607
|
+
stream open and read new image frames from it. The thread places the read images
|
|
608
|
+
in this object. An external application can retrieve the most recent one using
|
|
609
|
+
get_image().
|
|
610
|
+
|
|
611
|
+
Parameters
|
|
612
|
+
----------
|
|
613
|
+
flip_vertical: bool
|
|
614
|
+
If True, the image will be flipped over the X axis before being sent from EnSight."""
|
|
615
|
+
if self._image_stream is not None:
|
|
616
|
+
return
|
|
617
|
+
self.connect()
|
|
618
|
+
self._image_stream = self._stub.GetImageStream(
|
|
619
|
+
ensight_pb2.ImageStreamRequest(flip_vertical=flip_vertical, chunk=True),
|
|
620
|
+
metadata=self._metadata(),
|
|
621
|
+
)
|
|
622
|
+
self._image_thread = threading.Thread(target=self._poll_images)
|
|
623
|
+
self._image_thread.daemon = True
|
|
624
|
+
self._image_thread.start()
|
|
625
|
+
|
|
626
|
+
def _put_image(self, the_image):
|
|
627
|
+
"""Store an image on this instance.
|
|
628
|
+
|
|
629
|
+
This method is used by threads to store the latest image they receive
|
|
630
|
+
so it can be accessed by get_image.
|
|
631
|
+
"""
|
|
632
|
+
self._image = the_image
|
|
633
|
+
self._image_number += 1
|
|
634
|
+
|
|
635
|
+
def image_stream_is_enabled(self):
|
|
636
|
+
"""Check to see if the image stream is enabled.
|
|
637
|
+
|
|
638
|
+
If an image stream has been successfully established via image_stream_enable(),
|
|
639
|
+
then this function returns True.
|
|
640
|
+
|
|
641
|
+
Returns
|
|
642
|
+
-------
|
|
643
|
+
(bool):
|
|
644
|
+
True if a ensightservice::ImageReply steam is active
|
|
645
|
+
"""
|
|
646
|
+
return self._image_stream is not None
|
|
647
|
+
|
|
648
|
+
def _poll_images(self):
|
|
649
|
+
"""Handle image streams.
|
|
650
|
+
|
|
651
|
+
This method is called by a Python thread to read imagery via the shared memory
|
|
652
|
+
transport system or the the ensightservice::ImageReply stream.
|
|
653
|
+
"""
|
|
654
|
+
try:
|
|
655
|
+
while self._stub is not None:
|
|
656
|
+
if self._shmem_client:
|
|
657
|
+
if self._shmem_module:
|
|
658
|
+
img = self._shmem_module.stream_lock(self._shmem_client)
|
|
659
|
+
else:
|
|
660
|
+
img = self.command("ensight_grpc_shmem.stream_lock(enscl._shmem_client)")
|
|
661
|
+
if type(img) is dict:
|
|
662
|
+
the_image = dict(
|
|
663
|
+
pixels=img["pixeldata"], width=img["width"], height=img["height"]
|
|
664
|
+
)
|
|
665
|
+
self._put_image(the_image)
|
|
666
|
+
if self._shmem_module:
|
|
667
|
+
self._shmem_module.stream_unlock(self._shmem_client)
|
|
668
|
+
else:
|
|
669
|
+
self.command(
|
|
670
|
+
"ensight_grpc_shmem.stream_unlock(enscl._shmem_client)",
|
|
671
|
+
do_eval=False,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if self._image_stream is not None:
|
|
675
|
+
img = self._image_stream.next()
|
|
676
|
+
buffer = img.pixels
|
|
677
|
+
|
|
678
|
+
while not img.final:
|
|
679
|
+
img = self._image_stream.next()
|
|
680
|
+
buffer += img.pixels
|
|
681
|
+
|
|
682
|
+
the_image = dict(pixels=buffer, width=img.width, height=img.height)
|
|
683
|
+
self._put_image(the_image)
|
|
684
|
+
except Exception:
|
|
685
|
+
# signal that the gRPC connection has broken
|
|
686
|
+
self._image_stream = None
|
|
687
|
+
self._image_thread = None
|
|
688
|
+
self._image = None
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class _EnSightSubServicer(ensight_pb2_grpc.EnSightSubscriptionServicer):
|
|
692
|
+
"""Internal class handling reverse subscription connections.
|
|
693
|
+
The EnSight gRPC interface has a mechanism for reversing the gRPC
|
|
694
|
+
streams called Subscriptions. Image and event streams can be
|
|
695
|
+
subscribed to. In this mode, the client application starts a
|
|
696
|
+
gRPC server that implements the EnSightSubscription protocol.
|
|
697
|
+
EnSight will connect back to the client using this protocol and
|
|
698
|
+
send images/events back to the client as regular (non-stream)
|
|
699
|
+
rpc calls. This can be useful in situations where it is difficult
|
|
700
|
+
keep a long-running stream alive.
|
|
701
|
+
The EnSightSubServicer class implements a gRPC server for the client application.
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
def __init__(self, parent: Optional["EnSightGRPC"] = None):
|
|
705
|
+
self._server: Optional["grpc.Server"] = None
|
|
706
|
+
self._uri: str = ""
|
|
707
|
+
self._parent = parent
|
|
708
|
+
|
|
709
|
+
def PublishEvent(self, request: Any, context: Any) -> "ensight_pb2.GenericResponse":
|
|
710
|
+
"""Publish an event to the remote server."""
|
|
711
|
+
if self._parent is not None:
|
|
712
|
+
self._parent._put_event(request)
|
|
713
|
+
return ensight_pb2.GenericResponse(str="Event Published")
|
|
714
|
+
|
|
715
|
+
def PublishImage(self, request_iterator: Any, context: Any) -> "ensight_pb2.GenericResponse":
|
|
716
|
+
"""Publish a single image (possibly in chucks) to the remote server."""
|
|
717
|
+
img: Any = request_iterator.next()
|
|
718
|
+
buffer = img.pixels
|
|
719
|
+
while not img.final:
|
|
720
|
+
img = request_iterator.next()
|
|
721
|
+
buffer += img.pixels
|
|
722
|
+
the_image = dict(pixels=buffer, width=img.width, height=img.height)
|
|
723
|
+
if self._parent is not None:
|
|
724
|
+
self._parent._put_image(the_image)
|
|
725
|
+
return ensight_pb2.GenericResponse(str="Image Published")
|
|
726
|
+
|
|
727
|
+
def start(self):
|
|
728
|
+
"""Start the gRPC server to be used for the EnSight Subscription Service."""
|
|
729
|
+
self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
|
730
|
+
ensight_pb2_grpc.add_EnSightSubscriptionServicer_to_server(self, self._server)
|
|
731
|
+
# Start the server on localhost with a random port
|
|
732
|
+
port = self._server.add_insecure_port("localhost:0")
|
|
733
|
+
self._uri = "localhost:" + str(port)
|
|
734
|
+
self._server.start()
|
|
@@ -6,7 +6,7 @@ import subprocess
|
|
|
6
6
|
import sys
|
|
7
7
|
import tempfile
|
|
8
8
|
from types import ModuleType
|
|
9
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
9
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
|
10
10
|
import uuid
|
|
11
11
|
|
|
12
12
|
import psutil
|
|
@@ -18,6 +18,199 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from ansys.api.pyensight import ensight_api
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
class OmniverseKitInstance:
|
|
22
|
+
"""Interface to an Omniverse application instance
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
pid : int
|
|
27
|
+
The process id of the launched instance
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, pid: int) -> None:
|
|
31
|
+
self._pid: Optional[int] = pid
|
|
32
|
+
print("CTOR:", pid)
|
|
33
|
+
|
|
34
|
+
def __del__(self) -> None:
|
|
35
|
+
"""Close down the instance on delete"""
|
|
36
|
+
self.close()
|
|
37
|
+
|
|
38
|
+
def close(self) -> None:
|
|
39
|
+
"""Shutdown the Omniverse instance
|
|
40
|
+
|
|
41
|
+
If the instance associated with this object is still running,
|
|
42
|
+
shut it down.
|
|
43
|
+
"""
|
|
44
|
+
if not self.is_running():
|
|
45
|
+
return
|
|
46
|
+
proc = psutil.Process(self._pid)
|
|
47
|
+
for child in proc.children(recursive=True):
|
|
48
|
+
if psutil.pid_exists(child.pid):
|
|
49
|
+
# This can be a race condition, so it is ok if the child is dead already
|
|
50
|
+
try:
|
|
51
|
+
child.kill()
|
|
52
|
+
except psutil.NoSuchProcess:
|
|
53
|
+
pass
|
|
54
|
+
# Same issue, this process might already be shutting down, so NoSuchProcess is ok.
|
|
55
|
+
try:
|
|
56
|
+
proc.kill()
|
|
57
|
+
except psutil.NoSuchProcess:
|
|
58
|
+
pass
|
|
59
|
+
self._pid = None
|
|
60
|
+
|
|
61
|
+
def is_running(self) -> bool:
|
|
62
|
+
"""Check if the instance is still running
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
bool
|
|
67
|
+
True if the instance is still running.
|
|
68
|
+
"""
|
|
69
|
+
if not self._pid:
|
|
70
|
+
return False
|
|
71
|
+
if psutil.pid_exists(self._pid):
|
|
72
|
+
return True
|
|
73
|
+
self._pid = None
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
|
|
78
|
+
"""
|
|
79
|
+
Use a combination of the current omniverse application and the information
|
|
80
|
+
in the local .nvidia-omniverse/config/omniverse.toml file to come up with
|
|
81
|
+
the pathname of a kit executable suitable for hosting another copy of the
|
|
82
|
+
ansys.geometry.server kit.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
Optional[str]
|
|
87
|
+
The pathname of a kit executable or None
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
# parse the toml config file for the location of the installed apps
|
|
91
|
+
try:
|
|
92
|
+
import tomllib
|
|
93
|
+
except ModuleNotFoundError:
|
|
94
|
+
import pip._vendor.tomli as tomllib
|
|
95
|
+
|
|
96
|
+
homedir = os.path.expanduser("~")
|
|
97
|
+
ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
|
|
98
|
+
if not os.path.exists(ov_config):
|
|
99
|
+
return None
|
|
100
|
+
# read the Omniverse configuration toml file
|
|
101
|
+
with open(ov_config, "r") as ov_file:
|
|
102
|
+
ov_data = ov_file.read()
|
|
103
|
+
config = tomllib.loads(ov_data)
|
|
104
|
+
appdir = config.get("paths", {}).get("library_root", fallback_directory)
|
|
105
|
+
|
|
106
|
+
# If we are running inside an Omniverse app, use that information
|
|
107
|
+
try:
|
|
108
|
+
import omni.kit.app
|
|
109
|
+
|
|
110
|
+
# get the current application
|
|
111
|
+
app = omni.kit.app.get_app()
|
|
112
|
+
app_name = app.get_app_filename().split(".")[-1]
|
|
113
|
+
app_version = app.get_app_version().split("-")[0]
|
|
114
|
+
# and where it is installed
|
|
115
|
+
appdir = os.path.join(appdir, f"{app_name}-{app_version}")
|
|
116
|
+
except ModuleNotFoundError:
|
|
117
|
+
# Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
|
|
118
|
+
target = None
|
|
119
|
+
target_version = None
|
|
120
|
+
for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
|
|
121
|
+
test_dir = os.path.dirname(d)
|
|
122
|
+
# the name will be something like "create-2023.2.3"
|
|
123
|
+
name = os.path.basename(test_dir).split("-")
|
|
124
|
+
if len(name) != 2:
|
|
125
|
+
continue
|
|
126
|
+
if name[0] not in ("kit", "create", "view"):
|
|
127
|
+
continue
|
|
128
|
+
if (target_version is None) or (name[1] > target_version):
|
|
129
|
+
target = test_dir
|
|
130
|
+
target_version = name[1]
|
|
131
|
+
if target is None:
|
|
132
|
+
return None
|
|
133
|
+
appdir = target
|
|
134
|
+
|
|
135
|
+
# Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
|
|
136
|
+
# Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
|
|
137
|
+
exe_names = ["kit.sh", "kit"]
|
|
138
|
+
if sys.platform.startswith("win"):
|
|
139
|
+
exe_names = ["kit.bat", "kit.exe"]
|
|
140
|
+
|
|
141
|
+
# look in 4 places...
|
|
142
|
+
for dir_name in [appdir, os.path.join(appdir, "kit")]:
|
|
143
|
+
for exe_name in exe_names:
|
|
144
|
+
if os.path.exists(os.path.join(dir_name, exe_name)):
|
|
145
|
+
return os.path.join(dir_name, exe_name)
|
|
146
|
+
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def launch_kit_instance(
|
|
151
|
+
kit_path: Optional[str] = None,
|
|
152
|
+
extension_paths: Optional[List[str]] = None,
|
|
153
|
+
extensions: Optional[List[str]] = None,
|
|
154
|
+
cli_options: Optional[List[str]] = None,
|
|
155
|
+
log_file: Optional[str] = None,
|
|
156
|
+
log_level: str = "warn",
|
|
157
|
+
) -> "OmniverseKitInstance":
|
|
158
|
+
"""Launch an Omniverse application instance
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
kit_path : Optional[str]
|
|
163
|
+
The full pathname of to a binary capable of serving as a kit runner.
|
|
164
|
+
extension_paths : Optional[List[str]]
|
|
165
|
+
List of directory names to include the in search for kits.
|
|
166
|
+
extensions : Optional[List[str]]
|
|
167
|
+
List of kit extensions to be loaded into the launched kit instance.
|
|
168
|
+
log_file : Optional[str]
|
|
169
|
+
The name of a text file where the logging information for the instance will be saved.
|
|
170
|
+
log_level : str
|
|
171
|
+
The level of the logging information to record: "verbose", "info", "warn", "error", "fatal",
|
|
172
|
+
the default is "warn".
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
OmniverseKitInstance
|
|
177
|
+
The object interface for the launched instance
|
|
178
|
+
|
|
179
|
+
Examples
|
|
180
|
+
--------
|
|
181
|
+
Run a simple, empty GUI kit instance.
|
|
182
|
+
|
|
183
|
+
>>> from ansys.pyensight.core.utils import omniverse
|
|
184
|
+
>>> ov = omniverse.launch_kit_instance(extensions=['omni.kit.uiapp'])
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
# build the command line
|
|
188
|
+
if not kit_path:
|
|
189
|
+
kit_path = find_kit_filename()
|
|
190
|
+
if not kit_path:
|
|
191
|
+
raise RuntimeError("Unable to find a suitable Omniverse kit install")
|
|
192
|
+
cmd = [kit_path]
|
|
193
|
+
if extension_paths:
|
|
194
|
+
for path in extension_paths:
|
|
195
|
+
cmd.extend(["--ext-folder", path])
|
|
196
|
+
if extensions:
|
|
197
|
+
for ext in extensions:
|
|
198
|
+
cmd.extend(["--enable", ext])
|
|
199
|
+
if cli_options:
|
|
200
|
+
for opt in cli_options:
|
|
201
|
+
cmd.append(opt)
|
|
202
|
+
if log_level not in ("verbose", "info", "warn", "error", "fatal"):
|
|
203
|
+
raise RuntimeError(f"Invalid logging level: {log_level}")
|
|
204
|
+
cmd.append(f"--/log/level={log_level}")
|
|
205
|
+
if log_file:
|
|
206
|
+
cmd.append(f"--/log/file={log_file}")
|
|
207
|
+
cmd.append("--/log/enabled=true")
|
|
208
|
+
# Launch the process
|
|
209
|
+
env_vars = os.environ.copy()
|
|
210
|
+
p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env_vars)
|
|
211
|
+
return OmniverseKitInstance(p.pid)
|
|
212
|
+
|
|
213
|
+
|
|
21
214
|
class Omniverse:
|
|
22
215
|
"""Provides the ``ensight.utils.omniverse`` interface.
|
|
23
216
|
|
|
@@ -56,79 +249,6 @@ class Omniverse:
|
|
|
56
249
|
self._interpreter: str = ""
|
|
57
250
|
self._status_filename: str = ""
|
|
58
251
|
|
|
59
|
-
@staticmethod
|
|
60
|
-
def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
|
|
61
|
-
"""
|
|
62
|
-
Use a combination of the current omniverse application and the information
|
|
63
|
-
in the local .nvidia-omniverse/config/omniverse.toml file to come up with
|
|
64
|
-
the pathname of a kit executable suitable for hosting another copy of the
|
|
65
|
-
ansys.geometry.server kit.
|
|
66
|
-
|
|
67
|
-
Returns
|
|
68
|
-
-------
|
|
69
|
-
Optional[str]
|
|
70
|
-
The pathname of a kit executable or None
|
|
71
|
-
|
|
72
|
-
"""
|
|
73
|
-
# parse the toml config file for the location of the installed apps
|
|
74
|
-
try:
|
|
75
|
-
import tomllib
|
|
76
|
-
except ModuleNotFoundError:
|
|
77
|
-
import pip._vendor.tomli as tomllib
|
|
78
|
-
|
|
79
|
-
homedir = os.path.expanduser("~")
|
|
80
|
-
ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
|
|
81
|
-
if not os.path.exists(ov_config):
|
|
82
|
-
return None
|
|
83
|
-
# read the Omniverse configuration toml file
|
|
84
|
-
with open(ov_config, "r") as ov_file:
|
|
85
|
-
ov_data = ov_file.read()
|
|
86
|
-
config = tomllib.loads(ov_data)
|
|
87
|
-
appdir = config.get("paths", {}).get("library_root", fallback_directory)
|
|
88
|
-
|
|
89
|
-
# If we are running inside an Omniverse app, use that information
|
|
90
|
-
try:
|
|
91
|
-
import omni.kit.app
|
|
92
|
-
|
|
93
|
-
# get the current application
|
|
94
|
-
app = omni.kit.app.get_app()
|
|
95
|
-
app_name = app.get_app_filename().split(".")[-1]
|
|
96
|
-
app_version = app.get_app_version().split("-")[0]
|
|
97
|
-
# and where it is installed
|
|
98
|
-
appdir = os.path.join(appdir, f"{app_name}-{app_version}")
|
|
99
|
-
except ModuleNotFoundError:
|
|
100
|
-
# Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
|
|
101
|
-
target = None
|
|
102
|
-
target_version = None
|
|
103
|
-
for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
|
|
104
|
-
test_dir = os.path.dirname(d)
|
|
105
|
-
# the name will be something like "create-2023.2.3"
|
|
106
|
-
name = os.path.basename(test_dir).split("-")
|
|
107
|
-
if len(name) != 2:
|
|
108
|
-
continue
|
|
109
|
-
if name[0] not in ("kit", "create", "view"):
|
|
110
|
-
continue
|
|
111
|
-
if (target_version is None) or (name[1] > target_version):
|
|
112
|
-
target = test_dir
|
|
113
|
-
target_version = name[1]
|
|
114
|
-
if target is None:
|
|
115
|
-
return None
|
|
116
|
-
appdir = target
|
|
117
|
-
|
|
118
|
-
# Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
|
|
119
|
-
# Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
|
|
120
|
-
exe_names = ["kit.sh", "kit"]
|
|
121
|
-
if sys.platform.startswith("win"):
|
|
122
|
-
exe_names = ["kit.bat", "kit.exe"]
|
|
123
|
-
|
|
124
|
-
# look in 4 places...
|
|
125
|
-
for dir_name in [appdir, os.path.join(appdir, "kit")]:
|
|
126
|
-
for exe_name in exe_names:
|
|
127
|
-
if os.path.exists(os.path.join(dir_name, exe_name)):
|
|
128
|
-
return os.path.join(dir_name, exe_name)
|
|
129
|
-
|
|
130
|
-
return None
|
|
131
|
-
|
|
132
252
|
def _check_modules(self) -> None:
|
|
133
253
|
"""Verify that the Python interpreter is correct
|
|
134
254
|
|
|
@@ -361,9 +481,15 @@ class Omniverse:
|
|
|
361
481
|
update_cmd += f"{prefix}timesteps=1"
|
|
362
482
|
prefix = "&"
|
|
363
483
|
if line_width != 0.0:
|
|
364
|
-
|
|
365
|
-
if self._ensight
|
|
366
|
-
|
|
484
|
+
add_linewidth = False
|
|
485
|
+
if isinstance(self._ensight, ModuleType):
|
|
486
|
+
add_linewidth = True
|
|
487
|
+
else:
|
|
488
|
+
# only in 2025 R2 and beyond
|
|
489
|
+
if self._ensight._session.ensight_version_check("2025 R2", exception=False):
|
|
490
|
+
add_linewidth = True
|
|
491
|
+
if add_linewidth:
|
|
492
|
+
update_cmd += f"{prefix}ANSYS_linewidth={line_width}"
|
|
367
493
|
prefix = "&"
|
|
368
494
|
self._check_modules()
|
|
369
495
|
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 =
|
|
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.
|
|
198
|
+
destination=self._destination, line_width=self.line_width
|
|
193
199
|
)
|
|
194
200
|
logging.info("Omniverse connection established.")
|
|
195
201
|
|
|
196
|
-
# parse the DSG
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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 =
|
|
45
|
-
use_lines: bool = False,
|
|
44
|
+
line_width: float = 0.0,
|
|
46
45
|
) -> None:
|
|
47
46
|
self._cleaned_index = 0
|
|
48
47
|
self._cleaned_names: dict = {}
|
|
@@ -61,7 +60,6 @@ class OmniverseWrapper(object):
|
|
|
61
60
|
self.destination = destination
|
|
62
61
|
|
|
63
62
|
self._line_width = line_width
|
|
64
|
-
self._use_lines = use_lines
|
|
65
63
|
|
|
66
64
|
@property
|
|
67
65
|
def destination(self) -> str:
|
|
@@ -82,10 +80,6 @@ class OmniverseWrapper(object):
|
|
|
82
80
|
def line_width(self, line_width: float) -> None:
|
|
83
81
|
self._line_width = line_width
|
|
84
82
|
|
|
85
|
-
@property
|
|
86
|
-
def use_lines(self) -> bool:
|
|
87
|
-
return self._use_lines
|
|
88
|
-
|
|
89
83
|
def shutdown(self) -> None:
|
|
90
84
|
"""
|
|
91
85
|
Shutdown the connection to Omniverse cleanly.
|
|
@@ -582,7 +576,8 @@ class OmniverseWrapper(object):
|
|
|
582
576
|
pbrShader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(color)
|
|
583
577
|
|
|
584
578
|
material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface")
|
|
585
|
-
UsdShade.MaterialBindingAPI(mesh
|
|
579
|
+
mat_binding_api = UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim())
|
|
580
|
+
mat_binding_api.Bind(material)
|
|
586
581
|
|
|
587
582
|
return material
|
|
588
583
|
|
|
@@ -639,7 +634,8 @@ class OmniverseWrapper(object):
|
|
|
639
634
|
cam = geom_cam.GetCamera()
|
|
640
635
|
# LOL, not sure why is might be correct, but so far it seems to work???
|
|
641
636
|
cam.focalLength = camera.fieldofview
|
|
642
|
-
|
|
637
|
+
dist = (target_pos - cam_pos).GetLength()
|
|
638
|
+
cam.clippingRange = Gf.Range1f(0.1 * dist, 10.0 * dist)
|
|
643
639
|
look_at = Gf.Matrix4d()
|
|
644
640
|
look_at.SetLookAt(cam_pos, target_pos, up_vec)
|
|
645
641
|
trans_row = look_at.GetRow(3)
|
|
@@ -723,14 +719,83 @@ class OmniverseUpdateHandler(UpdateHandler):
|
|
|
723
719
|
self._group_prims: Dict[int, Any] = dict()
|
|
724
720
|
self._root_prim = None
|
|
725
721
|
self._sent_textures = False
|
|
722
|
+
self._updated_camera = False
|
|
726
723
|
|
|
727
724
|
def add_group(self, id: int, view: bool = False) -> None:
|
|
728
725
|
super().add_group(id, view)
|
|
729
726
|
group = self.session.groups[id]
|
|
727
|
+
|
|
730
728
|
if not view:
|
|
729
|
+
# Capture changes in line/sphere sizes if it was not set from cli
|
|
730
|
+
width = self.get_dsg_cmd_attribute(group, "ANSYS_linewidth")
|
|
731
|
+
if width:
|
|
732
|
+
try:
|
|
733
|
+
self._omni.line_width = float(width)
|
|
734
|
+
except ValueError:
|
|
735
|
+
pass
|
|
736
|
+
|
|
731
737
|
parent_prim = self._group_prims[group.parent_id]
|
|
738
|
+
# get the EnSight object type and the transform matrix
|
|
732
739
|
obj_type = self.get_dsg_cmd_attribute(group, "ENS_OBJ_TYPE")
|
|
733
|
-
matrix =
|
|
740
|
+
matrix = group.matrix4x4
|
|
741
|
+
# Is this a "case" group (it will contain part of the camera view in the matrix)
|
|
742
|
+
if obj_type == "ENS_CASE":
|
|
743
|
+
if (not self.session.vrmode) and (not self._updated_camera):
|
|
744
|
+
# if in camera mode, we need to update the camera matrix so we can
|
|
745
|
+
# use the identity matrix on this group. The camera should have been
|
|
746
|
+
# created in the "view" handler
|
|
747
|
+
cam_name = "/Root/Cam"
|
|
748
|
+
cam_prim = self._omni._stage.GetPrimAtPath(cam_name) # type: ignore
|
|
749
|
+
geom_cam = UsdGeom.Camera(cam_prim)
|
|
750
|
+
# get the camera
|
|
751
|
+
cam = geom_cam.GetCamera()
|
|
752
|
+
c = cam.transform
|
|
753
|
+
m = Gf.Matrix4d(*matrix).GetTranspose()
|
|
754
|
+
# move the model transform to the camera transform
|
|
755
|
+
cam.transform = c * m.GetInverse()
|
|
756
|
+
# set the updated camera
|
|
757
|
+
geom_cam.SetFromCamera(cam)
|
|
758
|
+
# apply the inverse cam transform to move the center of interest
|
|
759
|
+
# from data space to camera space
|
|
760
|
+
coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
|
|
761
|
+
if coi_attr.IsValid():
|
|
762
|
+
coi_data = coi_attr.Get()
|
|
763
|
+
coi_cam = (
|
|
764
|
+
Gf.Vec4d(coi_data[0], coi_data[1], coi_data[2], 1.0)
|
|
765
|
+
* cam.transform.GetInverse()
|
|
766
|
+
)
|
|
767
|
+
coi_attr.Set(
|
|
768
|
+
Gf.Vec3d(
|
|
769
|
+
coi_cam[0] / coi_cam[3],
|
|
770
|
+
coi_cam[1] / coi_cam[3],
|
|
771
|
+
coi_cam[2] / coi_cam[3],
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
# use the camera view by default
|
|
775
|
+
self._omni._stage.GetRootLayer().customLayerData = { # type: ignore
|
|
776
|
+
"cameraSettings": {"boundCamera": "/Root/Cam"}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
# We only want to do this once
|
|
780
|
+
self._updated_camera = True
|
|
781
|
+
matrix = [
|
|
782
|
+
1.0,
|
|
783
|
+
0.0,
|
|
784
|
+
0.0,
|
|
785
|
+
0.0,
|
|
786
|
+
0.0,
|
|
787
|
+
1.0,
|
|
788
|
+
0.0,
|
|
789
|
+
0.0,
|
|
790
|
+
0.0,
|
|
791
|
+
0.0,
|
|
792
|
+
1.0,
|
|
793
|
+
0.0,
|
|
794
|
+
0.0,
|
|
795
|
+
0.0,
|
|
796
|
+
0.0,
|
|
797
|
+
1.0,
|
|
798
|
+
]
|
|
734
799
|
prim = self._omni.create_dsg_group(
|
|
735
800
|
group.name, parent_prim, matrix=matrix, obj_type=obj_type
|
|
736
801
|
)
|
|
@@ -801,40 +866,37 @@ class OmniverseUpdateHandler(UpdateHandler):
|
|
|
801
866
|
first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
|
|
802
867
|
mat_info=mat_info,
|
|
803
868
|
)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
line_color
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
self.session.cur_timeline[0] == self.session.time_limits[0]
|
|
836
|
-
),
|
|
837
|
-
)
|
|
869
|
+
command, verts, tcoords, var_cmd = part.line_rep()
|
|
870
|
+
if command is not None:
|
|
871
|
+
# If there are no triangle (ideally if these are not hidden line
|
|
872
|
+
# edges), then use the base color for the part. If there are
|
|
873
|
+
# triangles, then assume these are hidden line edges and use the
|
|
874
|
+
# line_color.
|
|
875
|
+
line_color = color
|
|
876
|
+
if has_triangles:
|
|
877
|
+
line_color = [
|
|
878
|
+
part.cmd.line_color[0] * part.cmd.diffuse,
|
|
879
|
+
part.cmd.line_color[1] * part.cmd.diffuse,
|
|
880
|
+
part.cmd.line_color[2] * part.cmd.diffuse,
|
|
881
|
+
part.cmd.line_color[3],
|
|
882
|
+
]
|
|
883
|
+
# TODO: texture coordinates on lines are current invalid in OV
|
|
884
|
+
var_cmd = None
|
|
885
|
+
tcoords = None
|
|
886
|
+
# Generate the lines
|
|
887
|
+
_ = self._omni.create_dsg_lines(
|
|
888
|
+
name,
|
|
889
|
+
obj_id,
|
|
890
|
+
part.hash,
|
|
891
|
+
parent_prim,
|
|
892
|
+
verts,
|
|
893
|
+
tcoords,
|
|
894
|
+
matrix=matrix,
|
|
895
|
+
diffuse=line_color,
|
|
896
|
+
variable=var_cmd,
|
|
897
|
+
timeline=self.session.cur_timeline,
|
|
898
|
+
first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
|
|
899
|
+
)
|
|
838
900
|
|
|
839
901
|
elif part.cmd.render == part.cmd.NODES:
|
|
840
902
|
command, verts, sizes, colors, var_cmd = part.point_rep()
|
|
@@ -875,6 +937,8 @@ class OmniverseUpdateHandler(UpdateHandler):
|
|
|
875
937
|
# Upload a material to the Omniverse server
|
|
876
938
|
self._omni.uploadMaterial()
|
|
877
939
|
self._sent_textures = False
|
|
940
|
+
# We want to update the camera a single time within this update
|
|
941
|
+
self._updated_camera = False
|
|
878
942
|
|
|
879
943
|
def end_update(self) -> None:
|
|
880
944
|
super().end_update()
|
|
@@ -132,7 +132,6 @@ class GLBSession(dsg_server.DSGSession):
|
|
|
132
132
|
"""
|
|
133
133
|
mesh = self._gltf.meshes[meshid]
|
|
134
134
|
for prim_idx, prim in enumerate(mesh.primitives):
|
|
135
|
-
# TODO: line width/point size
|
|
136
135
|
# POINTS, LINES, TRIANGLES, LINE_LOOP, LINE_STRIP, TRIANGLE_STRIP, TRIANGLE_FAN
|
|
137
136
|
mode = prim.mode
|
|
138
137
|
if mode not in (
|
|
@@ -147,9 +146,7 @@ class GLBSession(dsg_server.DSGSession):
|
|
|
147
146
|
self.warn(f"Unhandled connectivity detected: {mode}. Geometry skipped.")
|
|
148
147
|
continue
|
|
149
148
|
glb_materialid = prim.material
|
|
150
|
-
|
|
151
149
|
line_width = self._callback_handler._omni.line_width
|
|
152
|
-
# TODO: override from scene extension: ANSYS_linewidth
|
|
153
150
|
|
|
154
151
|
# GLB Prim -> DSG Part
|
|
155
152
|
part_name = f"{parentname}_prim{prim_idx}_"
|
|
@@ -583,7 +580,16 @@ class GLBSession(dsg_server.DSGSession):
|
|
|
583
580
|
view_pb.fieldofview = camera.perspective.yfov
|
|
584
581
|
view_pb.aspectratio = camera.aspectratio.aspectRatio
|
|
585
582
|
self._handle_update_command(cmd)
|
|
586
|
-
|
|
583
|
+
# walk the scene nodes RJF
|
|
584
|
+
scene = self._gltf.scenes[scene_idx]
|
|
585
|
+
try:
|
|
586
|
+
if self._callback_handler._omni.line_width == 0.0:
|
|
587
|
+
width = float(scene.extensions["ANSYS_linewidth"]["linewidth"])
|
|
588
|
+
self._callback_handler._omni.line_width = width
|
|
589
|
+
except (KeyError, ValueError):
|
|
590
|
+
# in the case where the extension does not exist or is mal-formed
|
|
591
|
+
pass
|
|
592
|
+
for node_id in scene.nodes:
|
|
587
593
|
self._walk_node(node_id, view_pb.id)
|
|
588
594
|
self._finish_part()
|
|
589
595
|
|
|
@@ -621,11 +627,22 @@ class GLBSession(dsg_server.DSGSession):
|
|
|
621
627
|
List[float]
|
|
622
628
|
The computed timeline.
|
|
623
629
|
"""
|
|
624
|
-
# if ANSYS_scene_time is used, time ranges will come from there
|
|
625
|
-
if "ANSYS_scene_time" in self._gltf.scenes[scene_idx].extensions:
|
|
626
|
-
return self._gltf.scenes[scene_idx].extensions["ANSYS_scene_time"]
|
|
627
|
-
# if there is only one scene, then use the input timeline
|
|
628
630
|
num_scenes = len(self._gltf.scenes)
|
|
631
|
+
# if ANSYS_scene_timevalue is used, time ranges will come from there
|
|
632
|
+
try:
|
|
633
|
+
t0 = self._gltf.scenes[scene_idx].extensions["ANSYS_scene_timevalue"]["timevalue"]
|
|
634
|
+
idx = scene_idx + 1
|
|
635
|
+
if idx >= num_scenes:
|
|
636
|
+
idx = scene_idx
|
|
637
|
+
t1 = self._gltf.scenes[idx].extensions["ANSYS_scene_timevalue"]["timevalue"]
|
|
638
|
+
else:
|
|
639
|
+
t1 = t0
|
|
640
|
+
return [t0, t1]
|
|
641
|
+
except KeyError:
|
|
642
|
+
# If we fail due to dictionary key issue, the extension does not exist or is
|
|
643
|
+
# improperly formatted.
|
|
644
|
+
pass
|
|
645
|
+
# if there is only one scene, then use the input timeline
|
|
629
646
|
if num_scenes == 1:
|
|
630
647
|
return input_timeline
|
|
631
648
|
# if the timeline has zero length, we make it the number of scenes
|
|
@@ -4,7 +4,7 @@ ansys/pyensight/core/deep_pixel_view.html,sha256=6u4mGOuzJiPze8N8pIKJsEGPv5y6-zb
|
|
|
4
4
|
ansys/pyensight/core/dockerlauncher.py,sha256=LmgEFEjl0l3D_JaeJu5bbGR28aluHYtzb0-BmrC7ta8,28251
|
|
5
5
|
ansys/pyensight/core/enscontext.py,sha256=GSKkjZt1QEPyHEQ59EEBgKGMik9vjCdR9coR4uX7fEw,12141
|
|
6
6
|
ansys/pyensight/core/enshell_grpc.py,sha256=-OxSdFI_p3DmQnqh1jT_a_aSh_w-EUD2IaWGKxrnyjI,17122
|
|
7
|
-
ansys/pyensight/core/ensight_grpc.py,sha256=
|
|
7
|
+
ansys/pyensight/core/ensight_grpc.py,sha256=IkntqzHmNzhADZ1VZJtWGpaB3ZlSThcovsbXyliW_Sk,29458
|
|
8
8
|
ansys/pyensight/core/ensobj.py,sha256=uDtM2KHcAwd4hu5pcUYWbSD729ApHGIvuqZhEq8PxTI,18558
|
|
9
9
|
ansys/pyensight/core/launch_ensight.py,sha256=iZJM6GdpzGRDLzrv1V2QZ5veIOpNSB5xPpJUFY7rBuo,10254
|
|
10
10
|
ansys/pyensight/core/launcher.py,sha256=ymwfixwoHO7_c4qOetqccQbZjGT1HjeA7jwPi2JxlmE,10585
|
|
@@ -19,10 +19,10 @@ ansys/pyensight/core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
19
19
|
ansys/pyensight/core/utils/adr.py,sha256=XslZhlwcrSGzOlnhzprOv3ju_ppxxsWBjCnQL5KiNms,3570
|
|
20
20
|
ansys/pyensight/core/utils/dsg_server.py,sha256=FfiTJ7XsTW3OhI2w7PKMX5SLVoznGgZ6In-Q_wpkggE,45017
|
|
21
21
|
ansys/pyensight/core/utils/export.py,sha256=UAJQcrElo3esQD0CWdxxjMQ8yE1vB4cdAhF33_uZfQw,22605
|
|
22
|
-
ansys/pyensight/core/utils/omniverse.py,sha256=
|
|
23
|
-
ansys/pyensight/core/utils/omniverse_cli.py,sha256=
|
|
24
|
-
ansys/pyensight/core/utils/omniverse_dsg_server.py,sha256=
|
|
25
|
-
ansys/pyensight/core/utils/omniverse_glb_server.py,sha256=
|
|
22
|
+
ansys/pyensight/core/utils/omniverse.py,sha256=Y16_nXjfymnXpJmddAkqNnVCWGr8RTjHqA6JSNz8GA8,18483
|
|
23
|
+
ansys/pyensight/core/utils/omniverse_cli.py,sha256=aVYu4HsSZBl7A9_xU3DfItNB-hpxCdMPXm_oMAOU_HQ,20261
|
|
24
|
+
ansys/pyensight/core/utils/omniverse_dsg_server.py,sha256=Bnc99LU2216Dk2t8v0rVNH8sJm8pQiYwqkcH1N5zTvE,38384
|
|
25
|
+
ansys/pyensight/core/utils/omniverse_glb_server.py,sha256=gP6NEmrmqB1ALFGGLkNwnIhn7JWHylRvyIb8JbXrbmA,29879
|
|
26
26
|
ansys/pyensight/core/utils/parts.py,sha256=222XFRCjLgH7hho-cK9JrGCg3-KlTf54KIgc7y50sTE,52173
|
|
27
27
|
ansys/pyensight/core/utils/query.py,sha256=OXKDbf1sOTX0sUvtKcp64LhVl-BcrEsE43w8uMxLOYI,19828
|
|
28
28
|
ansys/pyensight/core/utils/readers.py,sha256=cNNzrE5pjy4wpQKWAzIIJfJCSpY3HU43az02f5I3pVU,11968
|
|
@@ -30,7 +30,7 @@ ansys/pyensight/core/utils/support.py,sha256=QI3z9ex7zJxjFbkCPba9DWqWgPFIThORqr0
|
|
|
30
30
|
ansys/pyensight/core/utils/variables.py,sha256=ZUiJdDIeRcowrnLXaJQqGwA0RbrfXhc1s4o4v9A4PiY,95133
|
|
31
31
|
ansys/pyensight/core/utils/views.py,sha256=ZKhJ6vMT7Rdd4bwJ0egMYTV7-D7Q7I19fF2_j_CMQ0o,12489
|
|
32
32
|
ansys/pyensight/core/utils/resources/Materials/000_sky.exr,sha256=xAR1gFd2uxPZDnvgfegdhEhRaqKtZldQDiR_-1rHKO0,8819933
|
|
33
|
-
ansys_pyensight_core-0.9.
|
|
34
|
-
ansys_pyensight_core-0.9.
|
|
35
|
-
ansys_pyensight_core-0.9.
|
|
36
|
-
ansys_pyensight_core-0.9.
|
|
33
|
+
ansys_pyensight_core-0.9.2.dist-info/LICENSE,sha256=qQWivZ12ETN5l3QxvTARY-QI5eoRRlyHdwLlAj0Bg5I,1089
|
|
34
|
+
ansys_pyensight_core-0.9.2.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
|
35
|
+
ansys_pyensight_core-0.9.2.dist-info/METADATA,sha256=KkO1m505t8Zbu76uwkfORfA33vR2HpHwyros1mwRFvs,12113
|
|
36
|
+
ansys_pyensight_core-0.9.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|