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

1""" 

2xaml.py 

3--------- 

4 

5Load 3D XAMl files, an export option from Solidworks. 

6""" 

7 

8import collections 

9 

10import numpy as np 

11 

12from .. import transformations as tf 

13from .. import util, visual 

14 

15 

16def load_XAML(file_obj, *args, **kwargs): 

17 """ 

18 Load a 3D XAML file. 

19 

20 Parameters 

21 ---------- 

22 file_obj : file object 

23 Open XAML file. 

24 

25 Returns 

26 ---------- 

27 result : dict 

28 Kwargs for a Trimesh constructor. 

29 """ 

30 

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 

47 

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) 

61 

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)) 

65 

66 # the XML namespace 

67 ns = root.tag.split("}")[0] + "}" 

68 

69 # the linked lists our results are going in 

70 vertices = [] 

71 faces = [] 

72 colors = [] 

73 normals = [] 

74 

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")) 

82 

83 # convert the element into a (4,) np.uint8 RGBA color 

84 diffuse = element_to_color(diffuse) 

85 specular = element_to_color(specular) 

86 

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() 

105 

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) 

115 

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)) 

121 

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) 

127 

128 c_faces = np.array( 

129 g.attrib["TriangleIndices"].replace(",", " ").split(), dtype=np.int64 

130 ).reshape((-1, 3)) 

131 

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) 

137 

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) 

143 

144 return result 

145 

146 

147try: 

148 from lxml import etree 

149 

150 from .common import XML_PARSER_OPTIONS 

151 

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 

157 

158 _xaml_loaders = {"xaml": ExceptionWrapper(E)}