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

@@ -0,0 +1,695 @@
1
+ import logging
2
+ import os
3
+ import queue
4
+ import threading
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
8
+ from ansys.pyensight.core import ensight_grpc
9
+ import numpy
10
+
11
+
12
+ class Part(object):
13
+ def __init__(self, session: "DSGSession"):
14
+ """
15
+ This object roughly represents an EnSight "Part". It contains the connectivity,
16
+ coordinates, normals and texture coordinate information for one DSG entity
17
+
18
+ This object stores basic geometry information coming from the DSG protocol. The
19
+ update_geom() method can parse an "UpdateGeom" protobuffer and merges the results
20
+ into the Part object.
21
+
22
+ Parameters
23
+ ----------
24
+ session:
25
+ The DSG connection session object.
26
+ """
27
+ self.session = session
28
+ self.conn_tris = numpy.array([], dtype="int32")
29
+ self.conn_lines = numpy.array([], dtype="int32")
30
+ self.coords = numpy.array([], dtype="float32")
31
+ self.normals = numpy.array([], dtype="float32")
32
+ self.normals_elem = False
33
+ self.tcoords = numpy.array([], dtype="float32")
34
+ self.tcoords_var_id: Optional[int] = None
35
+ self.tcoords_elem = False
36
+ self.cmd: Optional[Any] = None
37
+ self.reset()
38
+
39
+ def reset(self, cmd: Any = None) -> None:
40
+ self.conn_tris = numpy.array([], dtype="int32")
41
+ self.conn_lines = numpy.array([], dtype="int32")
42
+ self.coords = numpy.array([], dtype="float32")
43
+ self.normals = numpy.array([], dtype="float32")
44
+ self.normals_elem = False
45
+ self.tcoords = numpy.array([], dtype="float32")
46
+ self.tcoords_var_id = None
47
+ self.tcoords_elem = False
48
+ self.cmd = cmd
49
+
50
+ def update_geom(self, cmd: dynamic_scene_graph_pb2.UpdateGeom) -> None:
51
+ """
52
+ Merge an update geometry command into the numpy buffers being cached in this object
53
+
54
+ Parameters
55
+ ----------
56
+ cmd:
57
+ This is an array update command. It could be for coordinates, normals, variables, connectivity, etc.
58
+ """
59
+ if cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.COORDINATES:
60
+ if self.coords.size != cmd.total_array_size:
61
+ self.coords = numpy.resize(self.coords, cmd.total_array_size)
62
+ self.coords[cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)] = cmd.flt_array
63
+ elif cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.TRIANGLES:
64
+ if self.conn_tris.size != cmd.total_array_size:
65
+ self.conn_tris = numpy.resize(self.conn_tris, cmd.total_array_size)
66
+ self.conn_tris[cmd.chunk_offset : cmd.chunk_offset + len(cmd.int_array)] = cmd.int_array
67
+ elif cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.LINES:
68
+ if self.conn_lines.size != cmd.total_array_size:
69
+ self.conn_lines = numpy.resize(self.conn_lines, cmd.total_array_size)
70
+ self.conn_lines[
71
+ cmd.chunk_offset : cmd.chunk_offset + len(cmd.int_array)
72
+ ] = cmd.int_array
73
+ elif (cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_NORMALS) or (
74
+ cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.NODE_NORMALS
75
+ ):
76
+ self.normals_elem = cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_NORMALS
77
+ if self.normals.size != cmd.total_array_size:
78
+ self.normals = numpy.resize(self.normals, cmd.total_array_size)
79
+ self.normals[cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)] = cmd.flt_array
80
+ elif (cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE) or (
81
+ cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.NODE_VARIABLE
82
+ ):
83
+ # Get the variable definition
84
+ if cmd.variable_id in self.session.variables:
85
+ self.tcoords_var_id = cmd.variable_id
86
+ self.tcoords_elem = (
87
+ cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE
88
+ )
89
+ if self.tcoords.size != cmd.total_array_size:
90
+ self.tcoords = numpy.resize(self.tcoords, cmd.total_array_size)
91
+ self.tcoords[
92
+ cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)
93
+ ] = cmd.flt_array
94
+ else:
95
+ self.tcoords_var_id = None
96
+
97
+ def build(self):
98
+ """
99
+ This function processes the geometry arrays and converts them into nodal representation.
100
+ It will duplicate triangles as needed (to preserve element normals) and will convert
101
+ variable data into texture coordinates.
102
+
103
+ Returns
104
+ -------
105
+ On failure, the method returns None for the first return value. The returned tuple is:
106
+
107
+ (part_command, vertices, connectivity, normals, tex_coords, var_command)
108
+
109
+ part_command: UPDATE_PART command object
110
+ vertices: numpy array of the nodal coordinates
111
+ connectivity: numpy array of the triangle indices into the vertices array
112
+ normals: numpy array of per vertex normal values (optional)
113
+ tcoords: numpy array of per vertex texture coordinates (optional)
114
+ var_command: UPDATE_VARIABLE command object for the variable the texture coordinate correspond to, if any
115
+ """
116
+ if self.cmd is None:
117
+ return None, None, None, None, None, None
118
+ if self.conn_lines.size:
119
+ self.session.log(
120
+ f"Note: part '{self.cmd.name}' contains lines which are not currently supported."
121
+ )
122
+ return None, None, None, None, None, None
123
+ verts = self.coords
124
+ if self.session.normalize_geometry and self.session.scene_bounds is not None:
125
+ midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
126
+ midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
127
+ midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
128
+ dx = self.session.scene_bounds[3] - self.session.scene_bounds[0]
129
+ dy = self.session.scene_bounds[4] - self.session.scene_bounds[1]
130
+ dz = self.session.scene_bounds[5] - self.session.scene_bounds[2]
131
+ s = dx
132
+ if dy > s:
133
+ s = dy
134
+ if dz > s:
135
+ s = dz
136
+ if s == 0:
137
+ s = 1.0
138
+ num_verts = int(verts.size / 3)
139
+ for i in range(num_verts):
140
+ j = i * 3
141
+ verts[j + 0] = (verts[j + 0] - midx) / s
142
+ verts[j + 1] = (verts[j + 1] - midy) / s
143
+ verts[j + 2] = (verts[j + 2] - midz) / s
144
+
145
+ conn = self.conn_tris
146
+ normals = self.normals
147
+ tcoords = None
148
+ if self.tcoords.size:
149
+ tcoords = self.tcoords
150
+ if self.tcoords_elem or self.normals_elem:
151
+ verts_per_prim = 3
152
+ num_prims = int(conn.size / verts_per_prim)
153
+ # "flatten" the triangles to move values from elements to nodes
154
+ new_verts = numpy.ndarray((num_prims * verts_per_prim * 3,), dtype="float32")
155
+ new_conn = numpy.ndarray((num_prims * verts_per_prim,), dtype="int32")
156
+ new_tcoords = None
157
+ if tcoords is not None:
158
+ # remember that the input values are 1D at this point, we will expand to 2D later
159
+ new_tcoords = numpy.ndarray((num_prims * verts_per_prim,), dtype="float32")
160
+ new_normals = None
161
+ if normals is not None:
162
+ if normals.size == 0:
163
+ self.session.log("Warning: zero length normals!")
164
+ else:
165
+ new_normals = numpy.ndarray((num_prims * verts_per_prim * 3,), dtype="float32")
166
+ j = 0
167
+ for i0 in range(num_prims):
168
+ for i1 in range(verts_per_prim):
169
+ idx = conn[i0 * verts_per_prim + i1]
170
+ # new connectivity (identity)
171
+ new_conn[j] = j
172
+ # copy the vertex
173
+ new_verts[j * 3 + 0] = verts[idx * 3 + 0]
174
+ new_verts[j * 3 + 1] = verts[idx * 3 + 1]
175
+ new_verts[j * 3 + 2] = verts[idx * 3 + 2]
176
+ if new_normals is not None:
177
+ if self.normals_elem:
178
+ # copy the normal associated with the face
179
+ new_normals[j * 3 + 0] = normals[i0 * 3 + 0]
180
+ new_normals[j * 3 + 1] = normals[i0 * 3 + 1]
181
+ new_normals[j * 3 + 2] = normals[i0 * 3 + 2]
182
+ else:
183
+ # copy the same normal as the vertex
184
+ new_normals[j * 3 + 0] = normals[idx * 3 + 0]
185
+ new_normals[j * 3 + 1] = normals[idx * 3 + 1]
186
+ new_normals[j * 3 + 2] = normals[idx * 3 + 2]
187
+ if new_tcoords is not None:
188
+ # remember, 1D texture coords at this point
189
+ if self.tcoords_elem:
190
+ # copy the texture coord associated with the face
191
+ new_tcoords[j] = tcoords[i0]
192
+ else:
193
+ # copy the same texture coord as the vertex
194
+ new_tcoords[j] = tcoords[idx]
195
+ j += 1
196
+ # new arrays.
197
+ verts = new_verts
198
+ conn = new_conn
199
+ normals = new_normals
200
+ if tcoords is not None:
201
+ tcoords = new_tcoords
202
+
203
+ var_cmd = None
204
+ # texture coords need transformation from variable value to [ST]
205
+ if tcoords is not None:
206
+ var_dsg_id = self.cmd.color_variableid
207
+ var_cmd = self.session.variables[var_dsg_id]
208
+ v_min = None
209
+ v_max = None
210
+ for lvl in var_cmd.levels:
211
+ if (v_min is None) or (v_min > lvl.value):
212
+ v_min = lvl.value
213
+ if (v_max is None) or (v_max < lvl.value):
214
+ v_max = lvl.value
215
+ var_minmax = [v_min, v_max]
216
+ # build a power of two x 1 texture
217
+ num_texels = int(len(var_cmd.texture) / 4)
218
+ half_texel = 1 / (num_texels * 2.0)
219
+ num_verts = int(verts.size / 3)
220
+ tmp = numpy.ndarray((num_verts * 2,), dtype="float32")
221
+ tmp.fill(0.5) # fill in the T coordinate...
222
+ tex_width = half_texel * 2 * (num_texels - 1) # center to center of num_texels
223
+ # if the range is 0, adjust the min by -1. The result is that the texture
224
+ # coords will get mapped to S=1.0 which is what EnSight does in this situation
225
+ if (var_minmax[1] - var_minmax[0]) == 0.0:
226
+ var_minmax[0] = var_minmax[0] - 1.0
227
+ var_width = var_minmax[1] - var_minmax[0]
228
+ for idx in range(num_verts):
229
+ # normalized S coord value (clamp)
230
+ s = (tcoords[idx] - var_minmax[0]) / var_width
231
+ if s < 0.0:
232
+ s = 0.0
233
+ if s > 1.0:
234
+ s = 1.0
235
+ # map to the texture range and set the S value
236
+ tmp[idx * 2] = s * tex_width + half_texel
237
+ tcoords = tmp
238
+
239
+ self.session.log(
240
+ f"Part '{self.cmd.name}' defined: {self.coords.size/3} verts, {self.conn_tris.size/3} tris, {self.conn_lines.size/2} lines."
241
+ )
242
+ command = self.cmd
243
+
244
+ return command, verts, conn, normals, tcoords, var_cmd
245
+
246
+
247
+ class UpdateHandler(object):
248
+ """
249
+ This class serves as the interface between a DSGSession and a hosting application.
250
+ The DSGSession processes the general aspects of the gRPC pipeline and collects the
251
+ various DSG objects into collections of: groups, variables, etc. It also coalesces
252
+ the individual array updates into a "Part" object which represents a single addressable
253
+ mesh chunk.
254
+ UpdateHandler methods are called as the various update happen, and it is called when
255
+ a mesh chunk has been entirely defined. In most scenarios, a subclass of UpdateHandler
256
+ is passed to the DSGSession to handshake the mesh data to the application target.
257
+ """
258
+
259
+ def __init__(self) -> None:
260
+ self._session: "DSGSession"
261
+
262
+ @property
263
+ def session(self) -> "DSGSession":
264
+ """The session object this handler has been associated with"""
265
+ return self._session
266
+
267
+ @session.setter
268
+ def session(self, session: "DSGSession") -> None:
269
+ self._session = session
270
+
271
+ def add_group(self, id: int, view: bool = False) -> None:
272
+ """Called when a new group command has been added: self.session.groups[id]"""
273
+ if view:
274
+ self.session.log(f"Adding view: {self.session.groups[id]}")
275
+ else:
276
+ self.session.log(f"Adding group: {self.session.groups[id].name}")
277
+
278
+ def add_variable(self, id: int) -> None:
279
+ """Called when a new group command has been added: self.session.variables[id]"""
280
+ self.session.log(f"Adding variable: {self.session.variables[id].name}")
281
+
282
+ def finalize_part(self, part: Part) -> None:
283
+ """Called when all the updates on a Part object have been completed.
284
+
285
+ Note: this superclass method should be called after the subclass has processed
286
+ the part geometry as the saved part command will be destroyed by this call.
287
+ """
288
+ if part.cmd:
289
+ self.session.log(f"Part finalized: {part.cmd.name}")
290
+ part.cmd = None
291
+
292
+ def start_connection(self) -> None:
293
+ """A new gRPC connection has been established: self.session.grpc"""
294
+ grpc = self.session.grpc
295
+ self.session.log(f"gRPC connection established to: {grpc.host}:{grpc.port}")
296
+
297
+ def end_connection(self) -> None:
298
+ """The previous gRPC connection has been closed"""
299
+ self.session.log("gRPC connection closed")
300
+
301
+ def begin_update(self) -> None:
302
+ """A new scene update is about to begin"""
303
+ self.session.log("Begin update ------------------------")
304
+
305
+ def end_update(self) -> None:
306
+ """The scene update is complete"""
307
+ self.session.log("End update ------------------------")
308
+
309
+ def get_dsg_cmd_attribute(self, obj: Any, name: str, default: Any = None) -> Optional[str]:
310
+ """Utility function to get an attribute from a DSG update object
311
+
312
+ Note: UpdateVariable and UpdateGroup commands support generic attributes
313
+ """
314
+ return obj.attributes.get(name, default)
315
+
316
+ def group_matrix(self, group: Any) -> Any:
317
+ matrix = group.matrix4x4
318
+ # The Case matrix is basically the camera transform. In vrmode, we only want
319
+ # the raw geometry, so use the identity matrix.
320
+ if (
321
+ self.get_dsg_cmd_attribute(group, "ENS_OBJ_TYPE") == "ENS_CASE"
322
+ ) and self.session.vrmode:
323
+ matrix = [
324
+ 1.0,
325
+ 0.0,
326
+ 0.0,
327
+ 0.0,
328
+ 0.0,
329
+ 1.0,
330
+ 0.0,
331
+ 0.0,
332
+ 0.0,
333
+ 0.0,
334
+ 1.0,
335
+ 0.0,
336
+ 0.0,
337
+ 0.0,
338
+ 0.0,
339
+ 1.0,
340
+ ]
341
+ return matrix
342
+
343
+
344
+ class DSGSession(object):
345
+ def __init__(
346
+ self,
347
+ port: int = 12345,
348
+ host: str = "127.0.0.1",
349
+ security_code: str = "",
350
+ verbose: int = 0,
351
+ normalize_geometry: bool = False,
352
+ vrmode: bool = False,
353
+ handler: UpdateHandler = UpdateHandler(),
354
+ ):
355
+ """
356
+ Manage a gRPC connection and link it to an UpdateHandler instance
357
+
358
+ This class makes a DSG gRPC connection via the specified port and host (leveraging
359
+ the passed security code). As DSG protobuffers arrive, they are merged into Part
360
+ object instances and the UpdateHandler is invoked to further process them.
361
+
362
+ Parameters
363
+ ----------
364
+ port : int
365
+ The port number the EnSight gRPC service is running on.
366
+ The default is ``12345``.
367
+ host : str
368
+ Name of the host that the EnSight gRPC service is running on.
369
+ The default is ``"127.0.0.1"``, which is the localhost.
370
+ security_code : str
371
+ Shared security code for validating the gRPC communication.
372
+ The default is ``""``.
373
+ verbose : int
374
+ The verbosity level. If set to 1 or higher the class will call logging.info
375
+ for log output. The default is ``0``.
376
+ normalize_geometry : bool
377
+ If True, the scene coordinates will be remapped into the volume [-1,-1,-1] - [1,1,1]
378
+ The default is not to remap coordinates.
379
+ vrmode : bool
380
+ If True, do not include the EnSight camera in the generated view group. The default
381
+ is to include the EnSight view in the scene transformations.
382
+ handler : UpdateHandler
383
+ This is an UpdateHandler subclass that is called back when the state of
384
+ a scene transfer changes. For example, methods are called when the
385
+ transfer begins or ends and when a Part (mesh block) is ready for processing.
386
+ """
387
+ super().__init__()
388
+ self._grpc = ensight_grpc.EnSightGRPC(port=port, host=host, secret_key=security_code)
389
+ self._callback_handler = handler
390
+ self._verbose = verbose
391
+ self._thread: Optional[threading.Thread] = None
392
+ self._message_queue: queue.Queue = queue.Queue() # Messages coming from EnSight
393
+ self._dsg_queue: Optional[queue.SimpleQueue] = None # Outgoing messages to EnSight
394
+ self._shutdown = False
395
+ self._dsg = None
396
+ self._normalize_geometry = normalize_geometry
397
+ self._vrmode = vrmode
398
+ self._mesh_block_count = 0
399
+ self._variables: Dict[int, Any] = dict()
400
+ self._groups: Dict[int, Any] = dict()
401
+ self._part: Part = Part(self)
402
+ self._scene_bounds: Optional[List] = None
403
+ self._callback_handler.session = self
404
+
405
+ @property
406
+ def scene_bounds(self) -> Optional[List]:
407
+ return self._scene_bounds
408
+
409
+ @property
410
+ def mesh_block_count(self) -> int:
411
+ return self._mesh_block_count
412
+
413
+ @property
414
+ def vrmode(self) -> bool:
415
+ return self._vrmode
416
+
417
+ @vrmode.setter
418
+ def vrmode(self, value: bool) -> None:
419
+ self._vrmode = value
420
+
421
+ @property
422
+ def normalize_geometry(self) -> bool:
423
+ return self._normalize_geometry
424
+
425
+ @normalize_geometry.setter
426
+ def normalize_geometry(self, value: bool) -> None:
427
+ self._normalize_geometry = value
428
+
429
+ @property
430
+ def variables(self) -> dict:
431
+ return self._variables
432
+
433
+ @property
434
+ def groups(self) -> dict:
435
+ return self._groups
436
+
437
+ @property
438
+ def part(self) -> Part:
439
+ return self._part
440
+
441
+ @property
442
+ def grpc(self) -> ensight_grpc.EnSightGRPC:
443
+ return self._grpc
444
+
445
+ def log(self, s: str, level: int = 0) -> None:
446
+ """Log a string to the logging system
447
+
448
+ If the message level is less than the current verbosity,
449
+ emit the message.
450
+ """
451
+ if level < self._verbose:
452
+ logging.info(s)
453
+
454
+ def start(self) -> int:
455
+ """Start a gRPC connection to an EnSight instance
456
+
457
+ Make a gRPC connection and start a DSG stream handler.
458
+
459
+ Returns
460
+ -------
461
+ 0 on success, -1 on an error.
462
+ """
463
+ # Start by setting up and verifying the connection
464
+ self._grpc.connect()
465
+ if not self._grpc.is_connected():
466
+ self.log(f"Unable to establish gRPC connection to: {self._grpc.host}:{self._grpc.port}")
467
+ return -1
468
+ # Streaming API requires an iterator, so we make one from a queue
469
+ # it also returns an iterator. self._dsg_queue is the input stream interface
470
+ # self._dsg is the returned stream iterator.
471
+ if self._dsg is not None:
472
+ return 0
473
+ self._dsg_queue = queue.SimpleQueue()
474
+ self._dsg = self._grpc.dynamic_scene_graph_stream(
475
+ iter(self._dsg_queue.get, None) # type:ignore
476
+ )
477
+ self._thread = threading.Thread(target=self._poll_messages)
478
+ if self._thread is not None:
479
+ self._thread.start()
480
+ self._callback_handler.start_connection()
481
+ return 0
482
+
483
+ def end(self):
484
+ """Stop a gRPC connection to the EnSight instance"""
485
+ self._callback_handler.end_connection()
486
+ self._grpc.stop_server()
487
+ self._shutdown = True
488
+ self._thread.join()
489
+ self._grpc.shutdown()
490
+ self._dsg = None
491
+ self._thread = None
492
+ self._dsg_queue = None
493
+
494
+ def is_shutdown(self):
495
+ """Check the service shutdown request status"""
496
+ return self._shutdown
497
+
498
+ def request_an_update(self, animation: bool = False, allow_spontaneous: bool = True) -> None:
499
+ """Start a DSG update
500
+ Send a command to the DSG protocol to "init" an update.
501
+
502
+ Parameters
503
+ ----------
504
+ animation:
505
+ if True, export all EnSight timesteps.
506
+ allow_spontaneous:
507
+ if True, allow EnSight to trigger async updates.
508
+ """
509
+ # Send an INIT command to trigger a stream of update packets
510
+ cmd = dynamic_scene_graph_pb2.SceneClientCommand()
511
+ cmd.command_type = dynamic_scene_graph_pb2.SceneClientCommand.INIT
512
+ # Allow EnSight push commands, but full scene only for now...
513
+ cmd.init.allow_spontaneous = allow_spontaneous
514
+ cmd.init.include_temporal_geometry = animation
515
+ cmd.init.allow_incremental_updates = False
516
+ cmd.init.maximum_chunk_size = 1024 * 1024
517
+ self._dsg_queue.put(cmd) # type:ignore
518
+ # Handle the update messages
519
+ self.handle_one_update()
520
+
521
+ def _poll_messages(self) -> None:
522
+ """Core interface to grab DSG events from gRPC and queue them for processing
523
+
524
+ This is run by a thread that is monitoring the dsg RPC call for update messages
525
+ it places them in _message_queue as it finds them. They are picked up by the
526
+ main thread via get_next_message()
527
+ """
528
+ while not self._shutdown:
529
+ try:
530
+ self._message_queue.put(next(self._dsg)) # type:ignore
531
+ except Exception:
532
+ self._shutdown = True
533
+ self.log("DSG connection broken, calling exit")
534
+ os._exit(0)
535
+
536
+ def _get_next_message(self, wait: bool = True) -> Any:
537
+ """Get the next queued up protobuffer message
538
+
539
+ Called by the main thread to get any messages that were pulled in from the
540
+ dsg stream and placed here by _poll_messages()
541
+ """
542
+ try:
543
+ return self._message_queue.get(block=wait)
544
+ except queue.Empty:
545
+ return None
546
+
547
+ def handle_one_update(self) -> None:
548
+ """Monitor the DSG stream and handle a single update operation
549
+
550
+ Wait until we get the scene update begin message. From there, reset the current
551
+ scene buckets and then parse all the incoming commands until we get the scene
552
+ update end command. At which point, save the generated stage (started in the
553
+ view command handler). Note: Parts are handled with an available bucket at all times.
554
+ When a new part update comes in or the scene update end happens, the part is "finished".
555
+ """
556
+ # An update starts with a UPDATE_SCENE_BEGIN command
557
+ cmd = self._get_next_message()
558
+ while (cmd is not None) and (
559
+ cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_BEGIN
560
+ ):
561
+ # Look for a begin command
562
+ cmd = self._get_next_message()
563
+
564
+ # Start anew
565
+ self._variables = {}
566
+ self._groups = {}
567
+ self._part = Part(self)
568
+ self._scene_bounds = None
569
+ self._mesh_block_count = 0 # reset when a new group shows up
570
+ self._callback_handler.begin_update()
571
+
572
+ # handle the various commands until UPDATE_SCENE_END
573
+ cmd = self._get_next_message()
574
+ while (cmd is not None) and (
575
+ cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_END
576
+ ):
577
+ self._handle_update_command(cmd)
578
+ cmd = self._get_next_message()
579
+
580
+ # Flush the last part
581
+ self._finish_part()
582
+
583
+ self._callback_handler.end_update()
584
+
585
+ def _handle_update_command(self, cmd: dynamic_scene_graph_pb2.SceneUpdateCommand) -> None:
586
+ """Dispatch out a scene update command to the proper handler
587
+
588
+ Given a command object, pull out the correct portion of the protobuffer union and
589
+ pass it to the appropriate handler.
590
+
591
+ Parameters
592
+ ----------
593
+ cmd:
594
+ The command to be dispatched.
595
+ """
596
+ name = "Unknown"
597
+ if cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.DELETE_ID:
598
+ name = "Delete IDs"
599
+ elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_PART:
600
+ name = "Part update"
601
+ tmp = cmd.update_part
602
+ self._handle_part(tmp)
603
+ elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GROUP:
604
+ name = "Group update"
605
+ tmp = cmd.update_group
606
+ self._handle_group(tmp)
607
+ elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GEOM:
608
+ name = "Geom update"
609
+ tmp = cmd.update_geom
610
+ self._part.update_geom(tmp)
611
+ elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VARIABLE:
612
+ name = "Variable update"
613
+ tmp = cmd.update_variable
614
+ self._handle_variable(tmp)
615
+ elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VIEW:
616
+ name = "View update"
617
+ tmp = cmd.update_view
618
+ self._handle_view(tmp)
619
+ elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_TEXTURE:
620
+ name = "Texture update"
621
+ self.log(f"{name} --------------------------")
622
+
623
+ def _finish_part(self) -> None:
624
+ """Complete the current part
625
+
626
+ There is always a part being modified. This method completes the current part, committing
627
+ it to the handler.
628
+ """
629
+ self._part.build()
630
+ self._callback_handler.finalize_part(self.part)
631
+ self._mesh_block_count += 1
632
+
633
+ def _handle_part(self, part: Any) -> None:
634
+ """Handle a DSG UPDATE_PART command
635
+
636
+ Finish the current part and set up the next part.
637
+
638
+ Parameters
639
+ ----------
640
+ part:
641
+ The command coming from the EnSight stream.
642
+ """
643
+ self._finish_part()
644
+ self._part.reset(part)
645
+
646
+ def _handle_group(self, group: Any) -> None:
647
+ """Handle a DSG UPDATE_GROUP command
648
+
649
+ Parameters
650
+ ----------
651
+ group:
652
+ The command coming from the EnSight stream.
653
+ """
654
+ # reset current mesh (part) count for unique "part" naming in USD
655
+ self._mesh_block_count = 0
656
+
657
+ # record the scene bounds in case they are needed later
658
+ self._groups[group.id] = group
659
+ bounds = group.attributes.get("ENS_SCENE_BOUNDS", None)
660
+ if bounds:
661
+ minmax = list()
662
+ for v in bounds.split(","):
663
+ try:
664
+ minmax.append(float(v))
665
+ except ValueError:
666
+ pass
667
+ if len(minmax) == 6:
668
+ self._scene_bounds = minmax
669
+ # callback
670
+ self._callback_handler.add_group(group.id)
671
+
672
+ def _handle_variable(self, var: Any) -> None:
673
+ """Handle a DSG UPDATE_VARIABLE command
674
+
675
+ Save off the EnSight variable DSG command object.
676
+
677
+ Parameters
678
+ ----------
679
+ var:
680
+ The command coming from the EnSight stream.
681
+ """
682
+ self._variables[var.id] = var
683
+ self._callback_handler.add_variable(var.id)
684
+
685
+ def _handle_view(self, view: Any) -> None:
686
+ """Handle a DSG UPDATE_VIEW command
687
+
688
+ Parameters
689
+ ----------s
690
+ view:
691
+ The command coming from the EnSight stream.
692
+ """
693
+ self._scene_bounds = None
694
+ self._groups[view.id] = view
695
+ self._callback_handler.add_group(view.id, view=True)