ansys-pyensight-core 0.8.1__py3-none-any.whl → 0.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ansys-pyensight-core might be problematic. Click here for more details.

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