ansys-pyensight-core 0.8.12__py3-none-any.whl → 0.9.0__py3-none-any.whl

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

Potentially problematic release.


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

@@ -1,625 +1,631 @@
1
- import io
2
- import logging
3
- import os
4
- import sys
5
- from typing import Any, List, Optional
6
- import uuid
7
-
8
- from PIL import Image
9
- from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
10
- import ansys.pyensight.core.utils.dsg_server as dsg_server
11
- import numpy
12
- import pygltflib
13
-
14
- sys.path.insert(0, os.path.dirname(__file__))
15
- from dsg_server import UpdateHandler # noqa: E402
16
-
17
-
18
- class GLBSession(dsg_server.DSGSession):
19
- def __init__(
20
- self,
21
- verbose: int = 0,
22
- normalize_geometry: bool = False,
23
- time_scale: float = 1.0,
24
- vrmode: bool = False,
25
- handler: UpdateHandler = UpdateHandler(),
26
- ):
27
- """
28
- Provide an interface to read a GLB file and link it to an UpdateHandler instance
29
-
30
- This class reads GLB files and provides the data to an UpdateHandler instance for
31
- further processing.
32
-
33
- Parameters
34
- ----------
35
- verbose : int
36
- The verbosity level. If set to 1 or higher the class will call logging.info
37
- for log output. The default is ``0``.
38
- normalize_geometry : bool
39
- If True, the scene coordinates will be remapped into the volume [-1,-1,-1] - [1,1,1]
40
- The default is not to remap coordinates.
41
- time_scale : float
42
- Scale time values by this factor after being read. The default is ``1.0``.
43
- vrmode : bool
44
- If True, do not include the camera in the output.
45
- handler : UpdateHandler
46
- This is an UpdateHandler subclass that is called back when the state of
47
- a scene transfer changes. For example, methods are called when the
48
- transfer begins or ends and when a Part (mesh block) is ready for processing.
49
- """
50
- super().__init__(
51
- verbose=verbose,
52
- normalize_geometry=normalize_geometry,
53
- time_scale=time_scale,
54
- vrmode=vrmode,
55
- handler=handler,
56
- )
57
- self._gltf: pygltflib.GLTF2 = pygltflib.GLTF2()
58
- self._id_num: int = 0
59
- self._node_idx: int = -1
60
- self._glb_textures: dict = {}
61
- self._scene_id: int = 0
62
-
63
- def _reset(self) -> None:
64
- """
65
- Reset the current state to prepare for a new dataset.
66
- """
67
- super()._reset()
68
- self._cur_timeline = [0.0, 0.0] # Start/End time for current update
69
- self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
70
- self._gltf = pygltflib.GLTF2()
71
- self._node_idx = -1
72
- self._id_num = 0
73
- self._glb_textures = {}
74
- self._scene_id = 0
75
-
76
- def _next_id(self) -> int:
77
- """Simple sequential number source
78
- Called whenever a unique integer is needed.
79
-
80
- Returns
81
- -------
82
- int
83
- A unique, monotonically increasing integer.
84
- """
85
- self._id_num += 1
86
- return self._id_num
87
-
88
- def _map_material(self, glb_materialid: int, part_pb: Any) -> None:
89
- """
90
- Apply various material properties to part protocol buffer.
91
-
92
- Parameters
93
- ----------
94
- glb_materialid : int
95
- The GLB material ID to use as the source information.
96
- part_pb : Any
97
- The DSG UpdatePart protocol buffer to update.
98
- """
99
- mat = self._gltf.materials[glb_materialid]
100
- color = [1.0, 1.0, 1.0, 1.0]
101
- # Change the color if we can find one
102
- if hasattr(mat, "pbrMetallicRoughness"):
103
- if hasattr(mat.pbrMetallicRoughness, "baseColorFactor"):
104
- color = mat.pbrMetallicRoughness.baseColorFactor
105
- part_pb.fill_color.extend(color)
106
- part_pb.line_color.extend(color)
107
- # Constants for now
108
- part_pb.ambient = 1.0
109
- part_pb.diffuse = 1.0
110
- part_pb.specular_intensity = 1.0
111
- # if the material maps to a variable, set the variable id for coloring
112
- glb_varid = self._find_variable_from_glb_mat(glb_materialid)
113
- if glb_varid:
114
- part_pb.color_variableid = glb_varid
115
-
116
- def _parse_mesh(self, meshid: int, parentid: int, parentname: str) -> None:
117
- """
118
- Walk a mesh id found in a "node" instance. This amounts to
119
- walking the list of "primitives" in the "meshes" list indexed
120
- by the meshid.
121
-
122
- Parameters
123
- ----------
124
- meshid: int
125
- The index of the mesh in the "meshes" list.
126
-
127
- parentid: int
128
- The DSG parent id.
129
-
130
- parentname: str
131
- The name of the GROUP parent of the meshes.
132
- """
133
- mesh = self._gltf.meshes[meshid]
134
- for prim_idx, prim in enumerate(mesh.primitives):
135
- # POINTS, LINES, LINE_LOOP, LINE_STRIP, TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN
136
- mode = prim.mode
137
- if mode not in (pygltflib.TRIANGLES, pygltflib.LINES, pygltflib.POINTS):
138
- self.warn(
139
- f"Unhandled connectivity {mode}. Currently only TRIANGLE and LINE connectivity is supported."
140
- )
141
- continue
142
- glb_materialid = prim.material
143
-
144
- # GLB Prim -> DSG Part
145
- part_name = f"{parentname}_prim{prim_idx}_"
146
- cmd, part_pb = self._create_pb("PART", parent_id=parentid, name=part_name)
147
- part_pb.render = dynamic_scene_graph_pb2.UpdatePart.RenderingMode.CONNECTIVITY
148
- part_pb.shading = dynamic_scene_graph_pb2.UpdatePart.ShadingMode.NODAL
149
- self._map_material(glb_materialid, part_pb)
150
- part_dsg_id = part_pb.id
151
- self._handle_update_command(cmd)
152
-
153
- # GLB Attributes -> DSG Geom
154
- conn = self._get_data(prim.indices, 0)
155
- cmd, conn_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
156
- if mode == pygltflib.TRIANGLES:
157
- conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.TRIANGLES
158
- elif mode == pygltflib.LINES:
159
- conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.LINES
160
- else:
161
- conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.POINTS
162
- conn_pb.int_array.extend(conn)
163
- conn_pb.chunk_offset = 0
164
- conn_pb.total_array_size = len(conn)
165
- self._handle_update_command(cmd)
166
-
167
- if prim.attributes.POSITION is not None:
168
- verts = self._get_data(prim.attributes.POSITION)
169
- cmd, verts_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
170
- verts_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.COORDINATES
171
- verts_pb.flt_array.extend(verts)
172
- verts_pb.chunk_offset = 0
173
- verts_pb.total_array_size = len(verts)
174
- self._handle_update_command(cmd)
175
-
176
- if prim.attributes.NORMAL is not None:
177
- normals = self._get_data(prim.attributes.NORMAL)
178
- cmd, normals_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
179
- normals_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.NODE_NORMALS
180
- normals_pb.flt_array.extend(normals)
181
- normals_pb.chunk_offset = 0
182
- normals_pb.total_array_size = len(normals)
183
- self._handle_update_command(cmd)
184
-
185
- if prim.attributes.TEXCOORD_0 is not None:
186
- # Note: texture coords are stored as VEC2, so we get 2 components back
187
- texcoords = self._get_data(prim.attributes.TEXCOORD_0, components=2)
188
- # we only want the 's' component of an s,t pairing
189
- texcoords = texcoords[::2]
190
- cmd, texcoords_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
191
- texcoords_pb.payload_type = (
192
- dynamic_scene_graph_pb2.UpdateGeom.ArrayType.NODE_VARIABLE
193
- )
194
- texcoords_pb.flt_array.extend(texcoords)
195
- texcoords_pb.chunk_offset = 0
196
- texcoords_pb.total_array_size = len(texcoords)
197
- glb_varid = self._find_variable_from_glb_mat(glb_materialid)
198
- if glb_varid:
199
- texcoords_pb.variable_id = glb_varid
200
- self._handle_update_command(cmd)
201
-
202
- def _get_data(
203
- self,
204
- accessorid: int,
205
- components: int = 3,
206
- ) -> numpy.ndarray:
207
- """
208
- Return the float buffer corresponding to the given accessorid. The id
209
- is usually obtained from a primitive: primitive.attributes.POSITION
210
- or primitive.attributes.NORMAL or primitive.attributes.TEXCOORD_0.
211
- It can also come from primitive.indices. In that case, the number of
212
- components needs to be set to 0.
213
-
214
- Parameters
215
- ----------
216
- accessorid: int
217
- The accessor index of the primitive.
218
-
219
- components: int
220
- The number of floats per vertex for the values 1,2,3 if the number
221
- of components is 0, read integer indices.
222
-
223
- Returns
224
- -------
225
- numpy.ndarray
226
- The float buffer corresponding to the nodal data or an int buffer of connectivity.
227
- """
228
- dtypes = {}
229
- dtypes[pygltflib.BYTE] = numpy.int8
230
- dtypes[pygltflib.UNSIGNED_BYTE] = numpy.uint8
231
- dtypes[pygltflib.SHORT] = numpy.int16
232
- dtypes[pygltflib.UNSIGNED_SHORT] = numpy.uint16
233
- dtypes[pygltflib.UNSIGNED_INT] = numpy.uint32
234
- dtypes[pygltflib.FLOAT] = numpy.float32
235
-
236
- binary_blob = self._gltf.binary_blob()
237
- accessor = self._gltf.accessors[accessorid]
238
- buffer_view = self._gltf.bufferViews[accessor.bufferView]
239
- dtype = numpy.float32
240
- data_dtype = dtypes[accessor.componentType]
241
- count = accessor.count * components
242
- # connectivity
243
- if components == 0:
244
- dtype = numpy.uint32
245
- count = accessor.count
246
- offset = buffer_view.byteOffset + accessor.byteOffset
247
- blob = binary_blob[offset : offset + buffer_view.byteLength]
248
- ret = numpy.frombuffer(blob, dtype=data_dtype, count=count)
249
- if data_dtype != dtype:
250
- return ret.astype(dtype)
251
- return ret
252
-
253
- def _walk_node(self, nodeid: int, parentid: int) -> None:
254
- """
255
- Given a node id (likely from walking a scenes array), walk the mesh
256
- objects in the node. A "node" has the keys "mesh" and "name".
257
-
258
- Each node has a single mesh object in it.
259
-
260
- Parameters
261
- ----------
262
- nodeid: int
263
- The node id to walk.
264
-
265
- parentid: int
266
- The DSG parent id.
267
-
268
- """
269
- node = self._gltf.nodes[nodeid]
270
- name = self._name(node)
271
- matrix = self._transform(node)
272
-
273
- # GLB node -> DSG Group
274
- cmd, group_pb = self._create_pb("GROUP", parent_id=parentid, name=name)
275
- group_pb.matrix4x4.extend(matrix)
276
- self._handle_update_command(cmd)
277
-
278
- if node.mesh is not None:
279
- self._parse_mesh(node.mesh, group_pb.id, name)
280
-
281
- # Handle node.rotation, node.translation, node.scale, node.matrix
282
- for child_id in node.children:
283
- self._walk_node(child_id, group_pb.id)
284
-
285
- def start_uploads(self, timeline: List[float]) -> None:
286
- """
287
- Begin an upload process for a potential collection of files.
288
-
289
- Parameters
290
- ----------
291
- timeline : List[float]
292
- The time values for the files span this range of values.
293
- """
294
- self._scene_id = self._next_id()
295
- self._cur_timeline = timeline
296
- self._callback_handler.begin_update()
297
- self._update_status_file()
298
-
299
- def end_uploads(self) -> None:
300
- """
301
- The upload process for the current collection of files is complete.
302
- """
303
- self._reset()
304
- self._update_status_file()
305
-
306
- def _find_variable_from_glb_mat(self, glb_material_id: int) -> Optional[int]:
307
- """
308
- Given a glb_material id, find the corresponding dsg variable id
309
-
310
- Parameters
311
- ----------
312
- glb_material_id : int
313
- The material id from the glb file.
314
-
315
- Returns
316
- -------
317
- Optional[int]
318
- The dsg variable id or None, if no variable is found.
319
- """
320
- value = self._glb_textures.get(glb_material_id, None)
321
- if value is not None:
322
- return value["pb"].id
323
- return None
324
-
325
- def upload_file(self, glb_filename: str, timeline: List[float] = [0.0, 0.0]) -> bool:
326
- """
327
- Parse a GLB file and call out to the handler to present the data
328
- to another interface (e.g. Omniverse)
329
-
330
- Parameters
331
- ----------
332
- timeline : List[float]
333
- The first and last time value for which the content of this file should be
334
- visible.
335
-
336
- glb_filename : str
337
- The name of the GLB file to parse
338
-
339
- Returns
340
- -------
341
- bool:
342
- returns True on success, False otherwise
343
- """
344
- try:
345
- ok = True
346
- self._gltf = pygltflib.GLTF2().load(glb_filename)
347
- self.log(f"File: {glb_filename} Info: {self._gltf.asset}")
348
-
349
- # check for GLTFWriter source
350
- if (self._gltf.asset.generator is None) or (
351
- ("GLTF Writer" not in self._gltf.asset.generator)
352
- and ("Ansys Ensight" not in self._gltf.asset.generator)
353
- ):
354
- self.error(
355
- f"Unable to process: {glb_filename} : Not written by GLTF Writer library"
356
- )
357
- return False
358
-
359
- # Walk texture nodes -> DSG Variable buffers
360
- for tex_idx, texture in enumerate(self._gltf.textures):
361
- image = self._gltf.images[texture.source]
362
- raw_png = self._gltf.get_data_from_buffer_uri(image.uri)
363
- png_img = Image.open(io.BytesIO(raw_png))
364
- raw_rgba = png_img.tobytes()
365
- raw_rgba = raw_rgba[0 : len(raw_rgba) // png_img.size[1]]
366
- var_name = "Variable_" + str(tex_idx)
367
- cmd, var_pb = self._create_pb("VARIABLE", parent_id=self._scene_id, name=var_name)
368
- var_pb.location = dynamic_scene_graph_pb2.UpdateVariable.VarLocation.NODAL
369
- var_pb.dimension = dynamic_scene_graph_pb2.UpdateVariable.VarDimension.SCALAR
370
- var_pb.undefined_value = -1e38
371
- var_pb.pal_interp = (
372
- dynamic_scene_graph_pb2.UpdateVariable.PaletteInterpolation.CONTINUOUS
373
- )
374
- var_pb.sub_levels = 0
375
- var_pb.undefined_display = (
376
- dynamic_scene_graph_pb2.UpdateVariable.UndefinedDisplay.AS_ZERO
377
- )
378
- var_pb.texture = raw_rgba
379
- colors = numpy.frombuffer(raw_rgba, dtype=numpy.uint8)
380
- colors.shape = (-1, 4)
381
- num = len(colors)
382
- levels = []
383
- for i, c in enumerate(colors):
384
- level = dynamic_scene_graph_pb2.VariableLevel()
385
- level.value = float(i) / float(num - 1)
386
- level.red = float(c[0]) / 255.0
387
- level.green = float(c[1]) / 255.0
388
- level.blue = float(c[2]) / 255.0
389
- level.alpha = float(c[3]) / 255.0
390
- levels.append(level)
391
- var_pb.levels.extend(levels)
392
- # create a map from GLB material index to glb
393
- d = dict(pb=var_pb, idx=tex_idx)
394
- # Find all the materials that map to this texture
395
- for mat_idx, mat in enumerate(self._gltf.materials):
396
- if not hasattr(mat, "pbrMetallicRoughness"):
397
- continue
398
- if not hasattr(mat.pbrMetallicRoughness, "baseColorTexture"):
399
- continue
400
- if not hasattr(mat.pbrMetallicRoughness.baseColorTexture, "index"):
401
- continue
402
- if mat.pbrMetallicRoughness.baseColorTexture.index == tex_idx:
403
- material_index = mat_idx
404
- # does this Variable/texture already exist?
405
- duplicate = None
406
- saved_id = var_pb.id
407
- saved_name = var_pb.name
408
- for key, value in self._glb_textures.items():
409
- var_pb.name = value["pb"].name
410
- var_pb.id = value["pb"].id
411
- if value["pb"] == var_pb:
412
- duplicate = key
413
- break
414
- var_pb.id = saved_id
415
- var_pb.name = saved_name
416
- # if a new texture, add the Variable and create an index to the material
417
- if duplicate is None:
418
- self._handle_update_command(cmd)
419
- self._glb_textures[material_index] = d
420
- else:
421
- # create an additional reference to this variable from this material
422
- self._glb_textures[material_index] = self._glb_textures[duplicate]
423
-
424
- # GLB file: general layout
425
- # scene: "default_index"
426
- # scenes: [scene_index].nodes -> [node ids]
427
- # was scene_id = self._gltf.scene
428
- num_scenes = len(self._gltf.scenes)
429
- for scene_idx in range(num_scenes):
430
- # GLB Scene -> DSG View
431
- cmd, view_pb = self._create_pb("VIEW", parent_id=self._scene_id)
432
- view_pb.lookat.extend([0.0, 0.0, -1.0])
433
- view_pb.lookfrom.extend([0.0, 0.0, 0.0])
434
- view_pb.upvector.extend([0.0, 1.0, 0.0])
435
- view_pb.timeline.extend(self._build_scene_timeline(scene_idx, timeline))
436
- if len(self._gltf.cameras) > 0:
437
- camera = self._gltf.cameras[0]
438
- if camera.type == "orthographic":
439
- view_pb.nearfar.extend(
440
- [float(camera.orthographic.znear), float(camera.orthographic.zfar)]
441
- )
442
- else:
443
- view_pb.nearfar.extend(
444
- [float(camera.perspective.znear), float(camera.perspective.zfar)]
445
- )
446
- view_pb.fieldofview = camera.perspective.yfov
447
- view_pb.aspectratio = camera.aspectratio.aspectRatio
448
- self._handle_update_command(cmd)
449
- for node_id in self._gltf.scenes[scene_idx].nodes:
450
- self._walk_node(node_id, view_pb.id)
451
- self._finish_part()
452
-
453
- self._callback_handler.end_update()
454
-
455
- except Exception as e:
456
- import traceback
457
-
458
- self.error(f"Unable to process: {glb_filename} : {e}")
459
- traceback_str = "".join(traceback.format_tb(e.__traceback__))
460
- logging.debug(f"Traceback: {traceback_str}")
461
- ok = False
462
-
463
- return ok
464
-
465
- def _build_scene_timeline(self, scene_idx: int, input_timeline: List[float]) -> List[float]:
466
- """
467
- For a given scene and externally supplied timeline, compute the timeline for the scene.
468
-
469
- If the ANSYS_scene_time extension is present, use that value.
470
- If there is only a single scene, return the supplied timeline.
471
- If the supplied timeline is empty, use an integer timeline based on the number of scenes in the GLB file.
472
- Carve up the timeline into chunks, one per scene.
473
-
474
- Parameters
475
- ----------
476
- scene_idx: int
477
- The index of the scene to compute for.
478
-
479
- input_timeline: List[float]
480
- An externally supplied timeline.
481
-
482
- Returns
483
- -------
484
- List[float]
485
- The computed timeline.
486
- """
487
- # if ANSYS_scene_time is used, time ranges will come from there
488
- if "ANSYS_scene_time" in self._gltf.scenes[scene_idx].extensions:
489
- return self._gltf.scenes[scene_idx].extensions["ANSYS_scene_time"]
490
- # if there is only one scene, then use the input timeline
491
- num_scenes = len(self._gltf.scenes)
492
- if num_scenes == 1:
493
- return input_timeline
494
- # if the timeline has zero length, we make it the number of scenes
495
- timeline = input_timeline
496
- if timeline[1] - timeline[0] <= 0.0:
497
- timeline = [0.0, float(num_scenes - 1)]
498
- # carve time into the input timeline.
499
- delta = (timeline[1] - timeline[0]) / float(num_scenes)
500
- output: List[float] = []
501
- output.append(float(scene_idx) * delta + timeline[0])
502
- output.append(output[0] + delta)
503
- return output
504
-
505
- @staticmethod
506
- def _transform(node: Any) -> List[float]:
507
- """
508
- Convert the node "matrix" or "translation", "rotation" and "scale" values into
509
- a 4x4 matrix representation.
510
-
511
- "nodes": [
512
- {
513
- "matrix": [
514
- 1,0,0,0,
515
- 0,1,0,0,
516
- 0,0,1,0,
517
- 5,6,7,1
518
- ],
519
- ...
520
- },
521
- {
522
- "translation":
523
- [ 0,0,0 ],
524
- "rotation":
525
- [ 0,0,0,1 ],
526
- "scale":
527
- [ 1,1,1 ]
528
- ...
529
- },
530
- ]
531
-
532
- Parameters
533
- ----------
534
- node: Any
535
- The node to compute the matrix transform for.
536
-
537
- Returns
538
- -------
539
- List[float]
540
- The 4x4 transformation matrix.
541
-
542
- """
543
- identity = numpy.identity(4)
544
- if node.matrix:
545
- tmp = numpy.array(node.matrix)
546
- tmp.shape = (4, 4)
547
- tmp = tmp.transpose()
548
- return list(tmp.flatten())
549
- if node.translation:
550
- identity[3][0] = node.translation[0]
551
- identity[3][1] = node.translation[1]
552
- identity[3][2] = node.translation[2]
553
- if node.rotation:
554
- # In GLB, the null quat is [0,0,0,1] so reverse the vector here
555
- q = [node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]]
556
- rot = numpy.array(
557
- [
558
- [q[0], -q[1], -q[2], -q[3]],
559
- [q[1], q[0], -q[3], q[2]],
560
- [q[2], q[3], q[0], -q[1]],
561
- [q[3], -q[2], q[1], q[0]],
562
- ]
563
- )
564
- identity = numpy.multiply(identity, rot)
565
- if node.scale:
566
- s = node.scale
567
- scale = numpy.array(
568
- [
569
- [s[0], 0.0, 0.0, 0.0],
570
- [0.0, s[1], 0.0, 0.0],
571
- [0.0, 0.0, s[2], 0.0],
572
- [0.0, 0.0, 0.0, 1.0],
573
- ]
574
- )
575
- identity = numpy.multiply(identity, scale)
576
- return list(identity.flatten())
577
-
578
- def _name(self, node: Any) -> str:
579
- """
580
- Given a GLB node object, return the name of the node. If the node does not
581
- have a name, give it a generated node.
582
-
583
- Parameters
584
- ----------
585
- node: Any
586
- The GLB node to get the name of.
587
-
588
- Returns
589
- -------
590
- str
591
- The name of the node.
592
- """
593
- if hasattr(node, "name") and node.name:
594
- return node.name
595
- self._node_idx += 1
596
- return f"Node_{self._node_idx}"
597
-
598
- def _create_pb(
599
- self, cmd_type: str, parent_id: int = -1, name: str = ""
600
- ) -> "dynamic_scene_graph_pb2.SceneUpdateCommand":
601
- cmd = dynamic_scene_graph_pb2.SceneUpdateCommand()
602
- if cmd_type == "PART":
603
- cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_PART
604
- subcmd = cmd.update_part
605
- subcmd.hash = str(uuid.uuid1())
606
- elif cmd_type == "GROUP":
607
- cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GROUP
608
- subcmd = cmd.update_group
609
- elif cmd_type == "VARIABLE":
610
- cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VARIABLE
611
- subcmd = cmd.update_variable
612
- elif cmd_type == "GEOM":
613
- cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GEOM
614
- subcmd = cmd.update_geom
615
- subcmd.hash = str(uuid.uuid1())
616
- elif cmd_type == "VIEW":
617
- cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VIEW
618
- subcmd = cmd.update_view
619
- subcmd.id = self._next_id()
620
- if parent_id >= 0:
621
- subcmd.parent_id = parent_id
622
- if cmd_type not in ("GEOM", "VIEW"):
623
- if name:
624
- subcmd.name = name
625
- return cmd, subcmd
1
+ import io
2
+ import logging
3
+ import os
4
+ import sys
5
+ from typing import Any, List, Optional
6
+ import uuid
7
+
8
+ from PIL import Image
9
+ from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
10
+ import ansys.pyensight.core.utils.dsg_server as dsg_server
11
+ import numpy
12
+ import pygltflib
13
+
14
+ sys.path.insert(0, os.path.dirname(__file__))
15
+ from dsg_server import UpdateHandler # noqa: E402
16
+
17
+
18
+ class GLBSession(dsg_server.DSGSession):
19
+ def __init__(
20
+ self,
21
+ verbose: int = 0,
22
+ normalize_geometry: bool = False,
23
+ time_scale: float = 1.0,
24
+ vrmode: bool = False,
25
+ handler: UpdateHandler = UpdateHandler(),
26
+ ):
27
+ """
28
+ Provide an interface to read a GLB file and link it to an UpdateHandler instance
29
+
30
+ This class reads GLB files and provides the data to an UpdateHandler instance for
31
+ further processing.
32
+
33
+ Parameters
34
+ ----------
35
+ verbose : int
36
+ The verbosity level. If set to 1 or higher the class will call logging.info
37
+ for log output. The default is ``0``.
38
+ normalize_geometry : bool
39
+ If True, the scene coordinates will be remapped into the volume [-1,-1,-1] - [1,1,1]
40
+ The default is not to remap coordinates.
41
+ time_scale : float
42
+ Scale time values by this factor after being read. The default is ``1.0``.
43
+ vrmode : bool
44
+ If True, do not include the camera in the output.
45
+ handler : UpdateHandler
46
+ This is an UpdateHandler subclass that is called back when the state of
47
+ a scene transfer changes. For example, methods are called when the
48
+ transfer begins or ends and when a Part (mesh block) is ready for processing.
49
+ """
50
+ super().__init__(
51
+ verbose=verbose,
52
+ normalize_geometry=normalize_geometry,
53
+ time_scale=time_scale,
54
+ vrmode=vrmode,
55
+ handler=handler,
56
+ )
57
+ self._gltf: pygltflib.GLTF2 = pygltflib.GLTF2()
58
+ self._id_num: int = 0
59
+ self._node_idx: int = -1
60
+ self._glb_textures: dict = {}
61
+ self._scene_id: int = 0
62
+
63
+ def _reset(self) -> None:
64
+ """
65
+ Reset the current state to prepare for a new dataset.
66
+ """
67
+ super()._reset()
68
+ self._cur_timeline = [0.0, 0.0] # Start/End time for current update
69
+ self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
70
+ self._gltf = pygltflib.GLTF2()
71
+ self._node_idx = -1
72
+ self._id_num = 0
73
+ self._glb_textures = {}
74
+ self._scene_id = 0
75
+
76
+ def _next_id(self) -> int:
77
+ """Simple sequential number source
78
+ Called whenever a unique integer is needed.
79
+
80
+ Returns
81
+ -------
82
+ int
83
+ A unique, monotonically increasing integer.
84
+ """
85
+ self._id_num += 1
86
+ return self._id_num
87
+
88
+ def _map_material(self, glb_materialid: int, part_pb: Any) -> None:
89
+ """
90
+ Apply various material properties to part protocol buffer.
91
+
92
+ Parameters
93
+ ----------
94
+ glb_materialid : int
95
+ The GLB material ID to use as the source information.
96
+ part_pb : Any
97
+ The DSG UpdatePart protocol buffer to update.
98
+ """
99
+ mat = self._gltf.materials[glb_materialid]
100
+ color = [1.0, 1.0, 1.0, 1.0]
101
+ # Change the color if we can find one
102
+ if hasattr(mat, "pbrMetallicRoughness"):
103
+ if hasattr(mat.pbrMetallicRoughness, "baseColorFactor"):
104
+ color = mat.pbrMetallicRoughness.baseColorFactor
105
+ part_pb.fill_color.extend(color)
106
+ part_pb.line_color.extend(color)
107
+ # Constants for now
108
+ part_pb.ambient = 1.0
109
+ part_pb.diffuse = 1.0
110
+ part_pb.specular_intensity = 1.0
111
+ # if the material maps to a variable, set the variable id for coloring
112
+ glb_varid = self._find_variable_from_glb_mat(glb_materialid)
113
+ if glb_varid:
114
+ part_pb.color_variableid = glb_varid
115
+
116
+ def _parse_mesh(self, meshid: int, parentid: int, parentname: str) -> None:
117
+ """
118
+ Walk a mesh id found in a "node" instance. This amounts to
119
+ walking the list of "primitives" in the "meshes" list indexed
120
+ by the meshid.
121
+
122
+ Parameters
123
+ ----------
124
+ meshid: int
125
+ The index of the mesh in the "meshes" list.
126
+
127
+ parentid: int
128
+ The DSG parent id.
129
+
130
+ parentname: str
131
+ The name of the GROUP parent of the meshes.
132
+ """
133
+ mesh = self._gltf.meshes[meshid]
134
+ for prim_idx, prim in enumerate(mesh.primitives):
135
+ # POINTS, LINES, LINE_LOOP, LINE_STRIP, TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN
136
+ mode = prim.mode
137
+ if mode not in (pygltflib.TRIANGLES, pygltflib.LINES, pygltflib.POINTS):
138
+ self.warn(
139
+ f"Unhandled connectivity {mode}. Currently only TRIANGLE and LINE connectivity is supported."
140
+ )
141
+ continue
142
+ glb_materialid = prim.material
143
+
144
+ # GLB Prim -> DSG Part
145
+ part_name = f"{parentname}_prim{prim_idx}_"
146
+ cmd, part_pb = self._create_pb("PART", parent_id=parentid, name=part_name)
147
+ part_pb.render = dynamic_scene_graph_pb2.UpdatePart.RenderingMode.CONNECTIVITY
148
+ part_pb.shading = dynamic_scene_graph_pb2.UpdatePart.ShadingMode.NODAL
149
+ self._map_material(glb_materialid, part_pb)
150
+ part_dsg_id = part_pb.id
151
+ self._handle_update_command(cmd)
152
+
153
+ # GLB Attributes -> DSG Geom
154
+ conn = self._get_data(prim.indices, 0)
155
+ cmd, conn_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
156
+ if mode == pygltflib.TRIANGLES:
157
+ conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.TRIANGLES
158
+ elif mode == pygltflib.LINES:
159
+ conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.LINES
160
+ else:
161
+ conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.POINTS
162
+ conn_pb.int_array.extend(conn)
163
+ conn_pb.chunk_offset = 0
164
+ conn_pb.total_array_size = len(conn)
165
+ self._handle_update_command(cmd)
166
+
167
+ if prim.attributes.POSITION is not None:
168
+ verts = self._get_data(prim.attributes.POSITION)
169
+ cmd, verts_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
170
+ verts_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.COORDINATES
171
+ verts_pb.flt_array.extend(verts)
172
+ verts_pb.chunk_offset = 0
173
+ verts_pb.total_array_size = len(verts)
174
+ self._handle_update_command(cmd)
175
+
176
+ if prim.attributes.NORMAL is not None:
177
+ normals = self._get_data(prim.attributes.NORMAL)
178
+ cmd, normals_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
179
+ normals_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.NODE_NORMALS
180
+ normals_pb.flt_array.extend(normals)
181
+ normals_pb.chunk_offset = 0
182
+ normals_pb.total_array_size = len(normals)
183
+ self._handle_update_command(cmd)
184
+
185
+ if prim.attributes.TEXCOORD_0 is not None:
186
+ # Note: texture coords are stored as VEC2, so we get 2 components back
187
+ texcoords = self._get_data(prim.attributes.TEXCOORD_0, components=2)
188
+ # we only want the 's' component of an s,t pairing
189
+ texcoords = texcoords[::2]
190
+ cmd, texcoords_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
191
+ texcoords_pb.payload_type = (
192
+ dynamic_scene_graph_pb2.UpdateGeom.ArrayType.NODE_VARIABLE
193
+ )
194
+ texcoords_pb.flt_array.extend(texcoords)
195
+ texcoords_pb.chunk_offset = 0
196
+ texcoords_pb.total_array_size = len(texcoords)
197
+ glb_varid = self._find_variable_from_glb_mat(glb_materialid)
198
+ if glb_varid:
199
+ texcoords_pb.variable_id = glb_varid
200
+ self._handle_update_command(cmd)
201
+
202
+ def _get_data(
203
+ self,
204
+ accessorid: int,
205
+ components: int = 3,
206
+ ) -> numpy.ndarray:
207
+ """
208
+ Return the float buffer corresponding to the given accessorid. The id
209
+ is usually obtained from a primitive: primitive.attributes.POSITION
210
+ or primitive.attributes.NORMAL or primitive.attributes.TEXCOORD_0.
211
+ It can also come from primitive.indices. In that case, the number of
212
+ components needs to be set to 0.
213
+
214
+ Parameters
215
+ ----------
216
+ accessorid: int
217
+ The accessor index of the primitive.
218
+
219
+ components: int
220
+ The number of floats per vertex for the values 1,2,3 if the number
221
+ of components is 0, read integer indices.
222
+
223
+ Returns
224
+ -------
225
+ numpy.ndarray
226
+ The float buffer corresponding to the nodal data or an int buffer of connectivity.
227
+ """
228
+ dtypes = {}
229
+ dtypes[pygltflib.BYTE] = numpy.int8
230
+ dtypes[pygltflib.UNSIGNED_BYTE] = numpy.uint8
231
+ dtypes[pygltflib.SHORT] = numpy.int16
232
+ dtypes[pygltflib.UNSIGNED_SHORT] = numpy.uint16
233
+ dtypes[pygltflib.UNSIGNED_INT] = numpy.uint32
234
+ dtypes[pygltflib.FLOAT] = numpy.float32
235
+
236
+ binary_blob = self._gltf.binary_blob()
237
+ accessor = self._gltf.accessors[accessorid]
238
+ buffer_view = self._gltf.bufferViews[accessor.bufferView]
239
+ dtype = numpy.float32
240
+ data_dtype = dtypes[accessor.componentType]
241
+ count = accessor.count * components
242
+ # connectivity
243
+ if components == 0:
244
+ dtype = numpy.uint32
245
+ count = accessor.count
246
+ offset = buffer_view.byteOffset + accessor.byteOffset
247
+ blob = binary_blob[offset : offset + buffer_view.byteLength]
248
+ ret = numpy.frombuffer(blob, dtype=data_dtype, count=count)
249
+ if data_dtype != dtype:
250
+ return ret.astype(dtype)
251
+ return ret
252
+
253
+ def _walk_node(self, nodeid: int, parentid: int) -> None:
254
+ """
255
+ Given a node id (likely from walking a scenes array), walk the mesh
256
+ objects in the node. A "node" has the keys "mesh" and "name".
257
+
258
+ Each node has a single mesh object in it.
259
+
260
+ Parameters
261
+ ----------
262
+ nodeid: int
263
+ The node id to walk.
264
+
265
+ parentid: int
266
+ The DSG parent id.
267
+
268
+ """
269
+ node = self._gltf.nodes[nodeid]
270
+ name = self._name(node)
271
+ matrix = self._transform(node)
272
+
273
+ # GLB node -> DSG Group
274
+ cmd, group_pb = self._create_pb("GROUP", parent_id=parentid, name=name)
275
+ group_pb.matrix4x4.extend(matrix)
276
+ self._handle_update_command(cmd)
277
+
278
+ if node.mesh is not None:
279
+ self._parse_mesh(node.mesh, group_pb.id, name)
280
+
281
+ # Handle node.rotation, node.translation, node.scale, node.matrix
282
+ for child_id in node.children:
283
+ self._walk_node(child_id, group_pb.id)
284
+
285
+ def start_uploads(self, timeline: List[float]) -> None:
286
+ """
287
+ Begin an upload process for a potential collection of files.
288
+
289
+ Parameters
290
+ ----------
291
+ timeline : List[float]
292
+ The time values for the files span this range of values.
293
+ """
294
+ self._scene_id = self._next_id()
295
+ self._cur_timeline = timeline
296
+ self._callback_handler.begin_update()
297
+ self._update_status_file()
298
+
299
+ def end_uploads(self) -> None:
300
+ """
301
+ The upload process for the current collection of files is complete.
302
+ """
303
+ self._reset()
304
+ self._update_status_file()
305
+
306
+ def _find_variable_from_glb_mat(self, glb_material_id: int) -> Optional[int]:
307
+ """
308
+ Given a glb_material id, find the corresponding dsg variable id
309
+
310
+ Parameters
311
+ ----------
312
+ glb_material_id : int
313
+ The material id from the glb file.
314
+
315
+ Returns
316
+ -------
317
+ Optional[int]
318
+ The dsg variable id or None, if no variable is found.
319
+ """
320
+ value = self._glb_textures.get(glb_material_id, None)
321
+ if value is not None:
322
+ return value["pb"].id
323
+ return None
324
+
325
+ def upload_file(self, glb_filename: str, timeline: List[float] = [0.0, 0.0]) -> bool:
326
+ """
327
+ Parse a GLB file and call out to the handler to present the data
328
+ to another interface (e.g. Omniverse)
329
+
330
+ Parameters
331
+ ----------
332
+ timeline : List[float]
333
+ The first and last time value for which the content of this file should be
334
+ visible.
335
+
336
+ glb_filename : str
337
+ The name of the GLB file to parse
338
+
339
+ Returns
340
+ -------
341
+ bool:
342
+ returns True on success, False otherwise
343
+ """
344
+ try:
345
+ ok = True
346
+ self._gltf = pygltflib.GLTF2().load(glb_filename)
347
+ self.log(f"File: {glb_filename} Info: {self._gltf.asset}")
348
+
349
+ # check for GLTFWriter source
350
+ if (self._gltf.asset.generator is None) or (
351
+ ("GLTF Writer" not in self._gltf.asset.generator)
352
+ and ("Ansys Ensight" not in self._gltf.asset.generator)
353
+ ):
354
+ self.error(
355
+ f"Unable to process: {glb_filename} : Not written by GLTF Writer library"
356
+ )
357
+ return False
358
+
359
+ # Walk texture nodes -> DSG Variable buffers
360
+ for tex_idx, texture in enumerate(self._gltf.textures):
361
+ image = self._gltf.images[texture.source]
362
+ if image.uri is None:
363
+ bv = self._gltf.bufferViews[image.bufferView]
364
+ raw_png = self._gltf.binary_blob()[
365
+ bv.byteOffset : bv.byteOffset + bv.byteLength
366
+ ]
367
+ else:
368
+ raw_png = self._gltf.get_data_from_buffer_uri(image.uri)
369
+ png_img = Image.open(io.BytesIO(raw_png))
370
+ raw_rgba = png_img.tobytes()
371
+ raw_rgba = raw_rgba[0 : len(raw_rgba) // png_img.size[1]]
372
+ var_name = "Variable_" + str(tex_idx)
373
+ cmd, var_pb = self._create_pb("VARIABLE", parent_id=self._scene_id, name=var_name)
374
+ var_pb.location = dynamic_scene_graph_pb2.UpdateVariable.VarLocation.NODAL
375
+ var_pb.dimension = dynamic_scene_graph_pb2.UpdateVariable.VarDimension.SCALAR
376
+ var_pb.undefined_value = -1e38
377
+ var_pb.pal_interp = (
378
+ dynamic_scene_graph_pb2.UpdateVariable.PaletteInterpolation.CONTINUOUS
379
+ )
380
+ var_pb.sub_levels = 0
381
+ var_pb.undefined_display = (
382
+ dynamic_scene_graph_pb2.UpdateVariable.UndefinedDisplay.AS_ZERO
383
+ )
384
+ var_pb.texture = raw_rgba
385
+ colors = numpy.frombuffer(raw_rgba, dtype=numpy.uint8)
386
+ colors.shape = (-1, 4)
387
+ num = len(colors)
388
+ levels = []
389
+ for i, c in enumerate(colors):
390
+ level = dynamic_scene_graph_pb2.VariableLevel()
391
+ level.value = float(i) / float(num - 1)
392
+ level.red = float(c[0]) / 255.0
393
+ level.green = float(c[1]) / 255.0
394
+ level.blue = float(c[2]) / 255.0
395
+ level.alpha = float(c[3]) / 255.0
396
+ levels.append(level)
397
+ var_pb.levels.extend(levels)
398
+ # create a map from GLB material index to glb
399
+ d = dict(pb=var_pb, idx=tex_idx)
400
+ # Find all the materials that map to this texture
401
+ for mat_idx, mat in enumerate(self._gltf.materials):
402
+ if not hasattr(mat, "pbrMetallicRoughness"):
403
+ continue
404
+ if not hasattr(mat.pbrMetallicRoughness, "baseColorTexture"):
405
+ continue
406
+ if not hasattr(mat.pbrMetallicRoughness.baseColorTexture, "index"):
407
+ continue
408
+ if mat.pbrMetallicRoughness.baseColorTexture.index == tex_idx:
409
+ material_index = mat_idx
410
+ # does this Variable/texture already exist?
411
+ duplicate = None
412
+ saved_id = var_pb.id
413
+ saved_name = var_pb.name
414
+ for key, value in self._glb_textures.items():
415
+ var_pb.name = value["pb"].name
416
+ var_pb.id = value["pb"].id
417
+ if value["pb"] == var_pb:
418
+ duplicate = key
419
+ break
420
+ var_pb.id = saved_id
421
+ var_pb.name = saved_name
422
+ # if a new texture, add the Variable and create an index to the material
423
+ if duplicate is None:
424
+ self._handle_update_command(cmd)
425
+ self._glb_textures[material_index] = d
426
+ else:
427
+ # create an additional reference to this variable from this material
428
+ self._glb_textures[material_index] = self._glb_textures[duplicate]
429
+
430
+ # GLB file: general layout
431
+ # scene: "default_index"
432
+ # scenes: [scene_index].nodes -> [node ids]
433
+ # was scene_id = self._gltf.scene
434
+ num_scenes = len(self._gltf.scenes)
435
+ for scene_idx in range(num_scenes):
436
+ # GLB Scene -> DSG View
437
+ cmd, view_pb = self._create_pb("VIEW", parent_id=self._scene_id)
438
+ view_pb.lookat.extend([0.0, 0.0, -1.0])
439
+ view_pb.lookfrom.extend([0.0, 0.0, 0.0])
440
+ view_pb.upvector.extend([0.0, 1.0, 0.0])
441
+ view_pb.timeline.extend(self._build_scene_timeline(scene_idx, timeline))
442
+ if len(self._gltf.cameras) > 0:
443
+ camera = self._gltf.cameras[0]
444
+ if camera.type == "orthographic":
445
+ view_pb.nearfar.extend(
446
+ [float(camera.orthographic.znear), float(camera.orthographic.zfar)]
447
+ )
448
+ else:
449
+ view_pb.nearfar.extend(
450
+ [float(camera.perspective.znear), float(camera.perspective.zfar)]
451
+ )
452
+ view_pb.fieldofview = camera.perspective.yfov
453
+ view_pb.aspectratio = camera.aspectratio.aspectRatio
454
+ self._handle_update_command(cmd)
455
+ for node_id in self._gltf.scenes[scene_idx].nodes:
456
+ self._walk_node(node_id, view_pb.id)
457
+ self._finish_part()
458
+
459
+ self._callback_handler.end_update()
460
+
461
+ except Exception as e:
462
+ import traceback
463
+
464
+ self.error(f"Unable to process: {glb_filename} : {e}")
465
+ traceback_str = "".join(traceback.format_tb(e.__traceback__))
466
+ logging.debug(f"Traceback: {traceback_str}")
467
+ ok = False
468
+
469
+ return ok
470
+
471
+ def _build_scene_timeline(self, scene_idx: int, input_timeline: List[float]) -> List[float]:
472
+ """
473
+ For a given scene and externally supplied timeline, compute the timeline for the scene.
474
+
475
+ If the ANSYS_scene_time extension is present, use that value.
476
+ If there is only a single scene, return the supplied timeline.
477
+ If the supplied timeline is empty, use an integer timeline based on the number of scenes in the GLB file.
478
+ Carve up the timeline into chunks, one per scene.
479
+
480
+ Parameters
481
+ ----------
482
+ scene_idx: int
483
+ The index of the scene to compute for.
484
+
485
+ input_timeline: List[float]
486
+ An externally supplied timeline.
487
+
488
+ Returns
489
+ -------
490
+ List[float]
491
+ The computed timeline.
492
+ """
493
+ # if ANSYS_scene_time is used, time ranges will come from there
494
+ if "ANSYS_scene_time" in self._gltf.scenes[scene_idx].extensions:
495
+ return self._gltf.scenes[scene_idx].extensions["ANSYS_scene_time"]
496
+ # if there is only one scene, then use the input timeline
497
+ num_scenes = len(self._gltf.scenes)
498
+ if num_scenes == 1:
499
+ return input_timeline
500
+ # if the timeline has zero length, we make it the number of scenes
501
+ timeline = input_timeline
502
+ if timeline[1] - timeline[0] <= 0.0:
503
+ timeline = [0.0, float(num_scenes - 1)]
504
+ # carve time into the input timeline.
505
+ delta = (timeline[1] - timeline[0]) / float(num_scenes)
506
+ output: List[float] = []
507
+ output.append(float(scene_idx) * delta + timeline[0])
508
+ output.append(output[0] + delta)
509
+ return output
510
+
511
+ @staticmethod
512
+ def _transform(node: Any) -> List[float]:
513
+ """
514
+ Convert the node "matrix" or "translation", "rotation" and "scale" values into
515
+ a 4x4 matrix representation.
516
+
517
+ "nodes": [
518
+ {
519
+ "matrix": [
520
+ 1,0,0,0,
521
+ 0,1,0,0,
522
+ 0,0,1,0,
523
+ 5,6,7,1
524
+ ],
525
+ ...
526
+ },
527
+ {
528
+ "translation":
529
+ [ 0,0,0 ],
530
+ "rotation":
531
+ [ 0,0,0,1 ],
532
+ "scale":
533
+ [ 1,1,1 ]
534
+ ...
535
+ },
536
+ ]
537
+
538
+ Parameters
539
+ ----------
540
+ node: Any
541
+ The node to compute the matrix transform for.
542
+
543
+ Returns
544
+ -------
545
+ List[float]
546
+ The 4x4 transformation matrix.
547
+
548
+ """
549
+ identity = numpy.identity(4)
550
+ if node.matrix:
551
+ tmp = numpy.array(node.matrix)
552
+ tmp.shape = (4, 4)
553
+ tmp = tmp.transpose()
554
+ return list(tmp.flatten())
555
+ if node.translation:
556
+ identity[3][0] = node.translation[0]
557
+ identity[3][1] = node.translation[1]
558
+ identity[3][2] = node.translation[2]
559
+ if node.rotation:
560
+ # In GLB, the null quat is [0,0,0,1] so reverse the vector here
561
+ q = [node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]]
562
+ rot = numpy.array(
563
+ [
564
+ [q[0], -q[1], -q[2], -q[3]],
565
+ [q[1], q[0], -q[3], q[2]],
566
+ [q[2], q[3], q[0], -q[1]],
567
+ [q[3], -q[2], q[1], q[0]],
568
+ ]
569
+ )
570
+ identity = numpy.multiply(identity, rot)
571
+ if node.scale:
572
+ s = node.scale
573
+ scale = numpy.array(
574
+ [
575
+ [s[0], 0.0, 0.0, 0.0],
576
+ [0.0, s[1], 0.0, 0.0],
577
+ [0.0, 0.0, s[2], 0.0],
578
+ [0.0, 0.0, 0.0, 1.0],
579
+ ]
580
+ )
581
+ identity = numpy.multiply(identity, scale)
582
+ return list(identity.flatten())
583
+
584
+ def _name(self, node: Any) -> str:
585
+ """
586
+ Given a GLB node object, return the name of the node. If the node does not
587
+ have a name, give it a generated node.
588
+
589
+ Parameters
590
+ ----------
591
+ node: Any
592
+ The GLB node to get the name of.
593
+
594
+ Returns
595
+ -------
596
+ str
597
+ The name of the node.
598
+ """
599
+ if hasattr(node, "name") and node.name:
600
+ return node.name
601
+ self._node_idx += 1
602
+ return f"Node_{self._node_idx}"
603
+
604
+ def _create_pb(
605
+ self, cmd_type: str, parent_id: int = -1, name: str = ""
606
+ ) -> "dynamic_scene_graph_pb2.SceneUpdateCommand":
607
+ cmd = dynamic_scene_graph_pb2.SceneUpdateCommand()
608
+ if cmd_type == "PART":
609
+ cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_PART
610
+ subcmd = cmd.update_part
611
+ subcmd.hash = str(uuid.uuid1())
612
+ elif cmd_type == "GROUP":
613
+ cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GROUP
614
+ subcmd = cmd.update_group
615
+ elif cmd_type == "VARIABLE":
616
+ cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VARIABLE
617
+ subcmd = cmd.update_variable
618
+ elif cmd_type == "GEOM":
619
+ cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GEOM
620
+ subcmd = cmd.update_geom
621
+ subcmd.hash = str(uuid.uuid1())
622
+ elif cmd_type == "VIEW":
623
+ cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VIEW
624
+ subcmd = cmd.update_view
625
+ subcmd.id = self._next_id()
626
+ if parent_id >= 0:
627
+ subcmd.parent_id = parent_id
628
+ if cmd_type not in ("GEOM", "VIEW"):
629
+ if name:
630
+ subcmd.name = name
631
+ return cmd, subcmd