hub/venv/lib/python3.7/site-packages/trimesh/exchange/xml_based.py

454 lines
17 KiB
Python

import numpy as np
import collections
import json
from .. import util
from .. import visual
from .. import transformations as tf
try:
import networkx as nx
except BaseException as E:
# create a dummy module which will raise the ImportError
# or other exception only when someone tries to use networkx
from ..exceptions import ExceptionModule
nx = ExceptionModule(E)
def load_XAML(file_obj, *args, **kwargs):
"""
Load a 3D XAML file.
Parameters
----------
file_obj : file object
Open, containing XAML file
Returns
----------
result : dict
kwargs for a trimesh constructor, including:
vertices: (n,3) np.float64, points in space
faces: (m,3) np.int64, indices of vertices
face_colors: (m,4) np.uint8, RGBA colors
vertex_normals: (n,3) np.float64, vertex normals
"""
def element_to_color(element):
"""
Turn an XML element into a (4,) np.uint8 RGBA color
"""
if element is None:
return visual.DEFAULT_COLOR
hexcolor = int(element.attrib['Color'].replace('#', ''), 16)
opacity = float(element.attrib['Opacity'])
rgba = [(hexcolor >> 16) & 0xFF,
(hexcolor >> 8) & 0xFF,
(hexcolor & 0xFF),
opacity * 0xFF]
rgba = np.array(rgba, dtype=np.uint8)
return rgba
def element_to_transform(element):
"""
Turn an XML element into a (4,4) np.float64
transformation matrix.
"""
try:
matrix = next(element.iter(
tag=ns + 'MatrixTransform3D')).attrib['Matrix']
matrix = np.array(matrix.split(),
dtype=np.float64).reshape((4, 4)).T
return matrix
except StopIteration:
# this will be raised if the MatrixTransform3D isn't in the passed
# elements tree
return np.eye(4)
# read the file and parse XML
file_data = file_obj.read()
root = etree.XML(file_data)
# the XML namespace
ns = root.tag.split('}')[0] + '}'
# the linked lists our results are going in
vertices = []
faces = []
colors = []
normals = []
# iterate through the element tree
# the GeometryModel3D tag contains a material and geometry
for geometry in root.iter(tag=ns + 'GeometryModel3D'):
# get the diffuse and specular colors specified in the material
color_search = './/{ns}{color}Material/*/{ns}SolidColorBrush'
diffuse = geometry.find(color_search.format(ns=ns,
color='Diffuse'))
specular = geometry.find(color_search.format(ns=ns,
color='Specular'))
# convert the element into a (4,) np.uint8 RGBA color
diffuse = element_to_color(diffuse)
specular = element_to_color(specular)
# to get the final transform of a component we'll have to traverse
# all the way back to the root node and save transforms we find
current = geometry
transforms = collections.deque()
# when the root node is reached its parent will be None and we stop
while current is not None:
# element.find will only return elements that are direct children
# of the current element as opposed to element.iter,
# which will return any depth of child
transform_element = current.find(ns + 'ModelVisual3D.Transform')
if transform_element is not None:
# we are traversing the tree backwards, so append new
# transforms to the left of the deque
transforms.appendleft(element_to_transform(transform_element))
# we are going from the lowest level of the tree to the highest
# this avoids having to traverse any branches that don't have
# geometry
current = current.getparent()
if len(transforms) == 0:
# no transforms in the tree mean an identity matrix
transform = np.eye(4)
elif len(transforms) == 1:
# one transform in the tree we can just use
transform = transforms.pop()
else:
# multiple transforms we apply all of them in order
transform = util.multi_dot(transforms)
# iterate through the contained mesh geometry elements
for g in geometry.iter(tag=ns + 'MeshGeometry3D'):
c_normals = np.array(g.attrib['Normals'].replace(',', ' ').split(),
dtype=np.float64).reshape((-1, 3))
c_vertices = np.array(
g.attrib['Positions'].replace(
',', ' ').split(), dtype=np.float64).reshape(
(-1, 3))
# bake in the transform as we're saving
c_vertices = tf.transform_points(c_vertices, transform)
c_faces = np.array(
g.attrib['TriangleIndices'].replace(
',', ' ').split(), dtype=np.int64).reshape(
(-1, 3))
# save data to a sequence
vertices.append(c_vertices)
faces.append(c_faces)
colors.append(np.tile(diffuse, (len(c_faces), 1)))
normals.append(c_normals)
# compile the results into clean numpy arrays
result = dict()
result['vertices'], result['faces'] = util.append_faces(vertices,
faces)
result['face_colors'] = np.vstack(colors)
result['vertex_normals'] = np.vstack(normals)
return result
def load_3DXML(file_obj, *args, **kwargs):
"""
Load a 3DXML scene into kwargs. 3DXML is a CAD format
that can be exported from Solidworks
Parameters
------------
file_obj : file object
Open and containing 3DXML data
Returns
-----------
kwargs : dict
Can be passed to trimesh.exchange.load.load_kwargs
"""
archive = util.decompress(file_obj, file_type='zip')
# a dictionary of file name : lxml etree
as_etree = {}
for k, v in archive.items():
# wrap in try statement, as sometimes 3DXML
# contains non- xml files, like JPG previews
try:
as_etree[k] = etree.XML(v.read())
except etree.XMLSyntaxError:
# move the file object back to the file start
v.seek(0)
# the file name of the root scene
root_file = as_etree['Manifest.xml'].find('{*}Root').text
# the etree of the scene layout
tree = as_etree[root_file]
# index of root element of directed acyclic graph
root_id = tree.find('{*}ProductStructure').attrib['root']
# load the materials library from the materials elements
colors = {}
material_tree = as_etree['CATMaterialRef.3dxml']
for MaterialDomain in material_tree.iter('{*}MaterialDomain'):
material_id = MaterialDomain.attrib['id']
material_file = MaterialDomain.attrib['associatedFile'].split(
'urn:3DXML:')[-1]
rend = as_etree[material_file].find(
"{*}Feature[@Alias='RenderingFeature']")
diffuse = rend.find("{*}Attr[@Name='DiffuseColor']")
# specular = rend.find("{*}Attr[@Name='SpecularColor']")
# emissive = rend.find("{*}Attr[@Name='EmissiveColor']")
rgb = (np.array(json.loads(
diffuse.attrib['Value'])) * 255).astype(np.uint8)
colors[material_id] = rgb
# copy indexes for instances of colors
for MaterialDomainInstance in material_tree.iter(
'{*}MaterialDomainInstance'):
instance = MaterialDomainInstance.find('{*}IsInstanceOf')
# colors[b.attrib['id']] = colors[instance.text]
for aggregate in MaterialDomainInstance.findall('{*}IsAggregatedBy'):
colors[aggregate.text] = colors[instance.text]
# references which hold the 3DXML scene structure as a dict
# element id : {key : value}
references = collections.defaultdict(dict)
# the 3DXML can specify different visual properties for occurrences
view = tree.find('{*}DefaultView')
for ViewProp in view.iter('{*}DefaultViewProperty'):
color = ViewProp.find('{*}GraphicProperties/' +
'{*}SurfaceAttributes/{*}Color')
if (color is None or
'RGBAColorType' not in color.attrib.values()):
continue
rgba = np.array([color.attrib[i]
for i in ['red',
'green',
'blue',
'alpha']],
dtype=np.float)
rgba = (rgba * 255).astype(np.uint8)
for occurrence in ViewProp.findall('{*}OccurenceId/{*}id'):
reference_id = occurrence.text.split('#')[-1]
references[reference_id]['color'] = rgba
# geometries will hold meshes
geometries = dict()
# get geometry
for ReferenceRep in tree.iter(tag='{*}ReferenceRep'):
# the str of an int that represents this meshes unique ID
part_id = ReferenceRep.attrib['id']
# which part file in the archive contains the geometry we care about
part_file = ReferenceRep.attrib['associatedFile'].split(':')[-1]
# load actual geometry
mesh_faces = []
mesh_colors = []
mesh_normals = []
mesh_vertices = []
# the geometry is stored in a Rep
for Rep in as_etree[part_file].iter('{*}Rep'):
faces = Rep.find('{*}Faces/{*}Face')
vertices = Rep.find('{*}VertexBuffer/{*}Positions')
if faces is None or vertices is None:
continue
# these are vertex normals
normals = Rep.find('{*}VertexBuffer/{*}Normals')
material = Rep.find('{*}SurfaceAttributes/' +
'{*}MaterialApplication/' +
'{*}MaterialId')
(material_file, material_id) = material.attrib['id'].split(
'urn:3DXML:')[-1].split('#')
# triangle strips, sequence of arbitrary length lists
# np.fromstring is substantially faster than np.array(i.split())
# inside the list comprehension
strips = [np.fromstring(i, sep=' ', dtype=np.int64)
for i in faces.attrib['strips'].split(',')]
# convert strips to (m,3) int
mesh_faces.append(util.triangle_strips_to_faces(strips))
# they mix delimiters like we couldn't figure it out from the
# shape :(
# load vertices into (n, 3) float64
mesh_vertices.append(np.fromstring(
vertices.text.replace(',', ' '),
sep=' ',
dtype=np.float64).reshape((-1, 3)))
# load vertex normals into (n, 3) float64
mesh_normals.append(np.fromstring(
normals.text.replace(',', ' '),
sep=' ',
dtype=np.float64).reshape((-1, 3)))
# store the material information as (m,3) uint8 FACE COLORS
mesh_colors.append(np.tile(colors[material_id],
(len(mesh_faces[-1]), 1)))
# save each mesh as the kwargs for a trimesh.Trimesh constructor
# aka, a Trimesh object can be created with trimesh.Trimesh(**mesh)
# this avoids needing trimesh- specific imports in this IO function
mesh = dict()
(mesh['vertices'],
mesh['faces']) = util.append_faces(mesh_vertices,
mesh_faces)
mesh['vertex_normals'] = np.vstack(mesh_normals)
mesh['face_colors'] = np.vstack(mesh_colors)
# as far as I can tell, all 3DXML files are exported as
# implicit millimeters (it isn't specified in the file)
mesh['metadata'] = {'units': 'mm'}
mesh['class'] = 'Trimesh'
geometries[part_id] = mesh
references[part_id]['geometry'] = part_id
# a Reference3D maps to a subassembly or assembly
for Reference3D in tree.iter('{*}Reference3D'):
references[Reference3D.attrib['id']] = {
'name': Reference3D.attrib['name'],
'type': 'Reference3D'}
# a node that is the connectivity between a geometry and the Reference3D
for InstanceRep in tree.iter('{*}InstanceRep'):
current = InstanceRep.attrib['id']
instance = InstanceRep.find('{*}IsInstanceOf').text
aggregate = InstanceRep.find('{*}IsAggregatedBy').text
references[current].update({'aggregate': aggregate,
'instance': instance,
'type': 'InstanceRep'})
# an Instance3D maps basically to a part
for Instance3D in tree.iter('{*}Instance3D'):
matrix = np.eye(4)
relative = Instance3D.find('{*}RelativeMatrix')
if relative is not None:
relative = np.array(relative.text.split(),
dtype=np.float64)
# rotation component
matrix[:3, :3] = relative[:9].reshape((3, 3)).T
# translation component
matrix[:3, 3] = relative[9:]
current = Instance3D.attrib['id']
name = Instance3D.attrib['name']
instance = Instance3D.find('{*}IsInstanceOf').text
aggregate = Instance3D.find('{*}IsAggregatedBy').text
references[current].update({'aggregate': aggregate,
'instance': instance,
'matrix': matrix,
'name': name,
'type': 'Instance3D'})
# turn references into directed graph for path finding
graph = nx.DiGraph()
for k, v in references.items():
# IsAggregatedBy points up to a parent
if 'aggregate' in v:
graph.add_edge(v['aggregate'], k)
# IsInstanceOf indicates a child
if 'instance' in v:
graph.add_edge(k, v['instance'])
# the 3DXML format is stored as a directed acyclic graph that needs all
# paths from the root to a geometry to generate the tree of the scene
paths = []
for geometry_id in geometries.keys():
paths.extend(nx.all_simple_paths(graph,
source=root_id,
target=geometry_id))
# the name of the root frame
root_name = references[root_id]['name']
# create a list of kwargs to send to the scene.graph.update function
# start with a transform from the graphs base frame to our root name
graph_kwargs = [{'frame_to': root_name,
'matrix': np.eye(4)}]
# we are going to collect prettier geometry names as we traverse paths
geom_names = {}
# loop through every simple path and generate transforms tree
# note that we are flattening the transform tree here
for path_index, path in enumerate(paths):
name = ''
if 'name' in references[path[-3]]:
name = references[path[-3]]['name']
geom_names[path[-1]] = name
# we need a unique node name for our geometry instance frame
# due to the nature of the DAG names specified by the file may not
# be unique, so we add an Instance3D name then append the path ids
node_name = name + '#' + ':'.join(path)
# pull all transformations in the path
matrices = [references[i]['matrix']
for i in path if 'matrix' in references[i]]
if len(matrices) == 0:
matrix = np.eye(4)
elif len(matrices) == 1:
matrix = matrices[0]
else:
matrix = util.multi_dot(matrices)
graph_kwargs.append({'matrix': matrix,
'frame_from': root_name,
'frame_to': node_name,
'geometry': path[-1]})
# remap geometry names from id numbers to the name string
# we extracted from the 3DXML tree
geom_final = {}
for key, value in geometries.items():
if key in geom_names:
geom_final[geom_names[key]] = value
# change geometry names in graph kwargs in place
for kwarg in graph_kwargs:
if 'geometry' not in kwarg:
continue
kwarg['geometry'] = geom_names[kwarg['geometry']]
# create the kwargs for load_kwargs
result = {'class': 'Scene',
'geometry': geom_final,
'graph': graph_kwargs}
return result
def print_element(element):
"""
Pretty- print an lxml.etree element.
Parameters
------------
element : etree element
"""
pretty = etree.tostring(
element, pretty_print=True).decode('utf-8')
print(pretty)
return pretty
try:
from lxml import etree
_xml_loaders = {'xaml': load_XAML,
'3dxml': load_3DXML}
except ImportError:
_xml_loaders = {}