Coverage for trimesh/exchange/xaml.py: 94%
66 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2xaml.py
3---------
5Load 3D XAMl files, an export option from Solidworks.
6"""
8import collections
10import numpy as np
12from .. import transformations as tf
13from .. import util, visual
16def load_XAML(file_obj, *args, **kwargs):
17 """
18 Load a 3D XAML file.
20 Parameters
21 ----------
22 file_obj : file object
23 Open XAML file.
25 Returns
26 ----------
27 result : dict
28 Kwargs for a Trimesh constructor.
29 """
31 def element_to_color(element):
32 """
33 Turn an XML element into a (4,) np.uint8 RGBA color
34 """
35 if element is None:
36 return visual.DEFAULT_COLOR
37 hexcolor = int(element.attrib["Color"].replace("#", ""), 16)
38 opacity = float(element.attrib["Opacity"])
39 rgba = [
40 (hexcolor >> 16) & 0xFF,
41 (hexcolor >> 8) & 0xFF,
42 (hexcolor & 0xFF),
43 opacity * 0xFF,
44 ]
45 rgba = np.array(rgba, dtype=np.uint8)
46 return rgba
48 def element_to_transform(element):
49 """
50 Turn an XML element into a (4,4) np.float64
51 transformation matrix.
52 """
53 try:
54 matrix = next(element.iter(tag=ns + "MatrixTransform3D")).attrib["Matrix"]
55 matrix = np.array(matrix.split(), dtype=np.float64).reshape((4, 4)).T
56 return matrix
57 except StopIteration:
58 # this will be raised if the MatrixTransform3D isn't in the passed
59 # elements tree
60 return np.eye(4)
62 # read the file and parse XML
63 file_data = file_obj.read()
64 root = etree.fromstring(file_data, parser=etree.XMLParser(**XML_PARSER_OPTIONS))
66 # the XML namespace
67 ns = root.tag.split("}")[0] + "}"
69 # the linked lists our results are going in
70 vertices = []
71 faces = []
72 colors = []
73 normals = []
75 # iterate through the element tree
76 # the GeometryModel3D tag contains a material and geometry
77 for geometry in root.iter(tag=ns + "GeometryModel3D"):
78 # get the diffuse and specular colors specified in the material
79 color_search = ".//{ns}{color}Material/*/{ns}SolidColorBrush"
80 diffuse = geometry.find(color_search.format(ns=ns, color="Diffuse"))
81 specular = geometry.find(color_search.format(ns=ns, color="Specular"))
83 # convert the element into a (4,) np.uint8 RGBA color
84 diffuse = element_to_color(diffuse)
85 specular = element_to_color(specular)
87 # to get the final transform of a component we'll have to traverse
88 # all the way back to the root node and save transforms we find
89 current = geometry
90 transforms = collections.deque()
91 # when the root node is reached its parent will be None and we stop
92 while current is not None:
93 # element.find will only return elements that are direct children
94 # of the current element as opposed to element.iter,
95 # which will return any depth of child
96 transform_element = current.find(ns + "ModelVisual3D.Transform")
97 if transform_element is not None:
98 # we are traversing the tree backwards, so append new
99 # transforms to the left of the deque
100 transforms.appendleft(element_to_transform(transform_element))
101 # we are going from the lowest level of the tree to the highest
102 # this avoids having to traverse any branches that don't have
103 # geometry
104 current = current.getparent()
106 if len(transforms) == 0:
107 # no transforms in the tree mean an identity matrix
108 transform = np.eye(4)
109 elif len(transforms) == 1:
110 # one transform in the tree we can just use
111 transform = transforms.pop()
112 else:
113 # multiple transforms we apply all of them in order
114 transform = util.multi_dot(transforms)
116 # iterate through the contained mesh geometry elements
117 for g in geometry.iter(tag=ns + "MeshGeometry3D"):
118 c_normals = np.array(
119 g.attrib["Normals"].replace(",", " ").split(), dtype=np.float64
120 ).reshape((-1, 3))
122 c_vertices = np.array(
123 g.attrib["Positions"].replace(",", " ").split(), dtype=np.float64
124 ).reshape((-1, 3))
125 # bake in the transform as we're saving
126 c_vertices = tf.transform_points(c_vertices, transform)
128 c_faces = np.array(
129 g.attrib["TriangleIndices"].replace(",", " ").split(), dtype=np.int64
130 ).reshape((-1, 3))
132 # save data to a sequence
133 vertices.append(c_vertices)
134 faces.append(c_faces)
135 colors.append(np.tile(diffuse, (len(c_faces), 1)))
136 normals.append(c_normals)
138 # compile the results into clean numpy arrays
139 result = {"units": "meters"}
140 result["vertices"], result["faces"] = util.append_faces(vertices, faces)
141 result["face_colors"] = np.vstack(colors)
142 result["vertex_normals"] = np.vstack(normals)
144 return result
147try:
148 from lxml import etree
150 from .common import XML_PARSER_OPTIONS
152 _xaml_loaders = {"xaml": load_XAML}
153except BaseException as E:
154 # create a dummy module which will raise the ImportError
155 # or other exception only when someone tries to use networkx
156 from ..exceptions import ExceptionWrapper
158 _xaml_loaders = {"xaml": ExceptionWrapper(E)}