Coverage for trimesh/path/exchange/misc.py: 94%

70 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1import numpy as np 

2 

3from ... import graph, grouping, util 

4from ...constants import tol_path 

5from ...typed import ArrayLike, NDArray 

6from ..entities import Arc, Line 

7 

8 

9def dict_to_path(as_dict): 

10 """ 

11 Turn a pure dict into a dict containing entity objects that 

12 can be sent directly to a Path constructor. 

13 

14 Parameters 

15 ------------ 

16 as_dict : dict 

17 Has keys: 'vertices', 'entities' 

18 

19 Returns 

20 ------------ 

21 kwargs : dict 

22 Has keys: 'vertices', 'entities' 

23 """ 

24 # start kwargs with initial value 

25 result = as_dict.copy() 

26 # map of constructors 

27 loaders = {"Arc": Arc, "Line": Line} 

28 # pre- allocate entity array 

29 entities = [None] * len(as_dict["entities"]) 

30 # run constructor for dict kwargs 

31 for entity_index, entity in enumerate(as_dict["entities"]): 

32 if entity["type"] == "Line": 

33 entities[entity_index] = loaders[entity["type"]](points=entity["points"]) 

34 else: 

35 entities[entity_index] = loaders[entity["type"]]( 

36 points=entity["points"], closed=entity["closed"] 

37 ) 

38 result["entities"] = entities 

39 

40 return result 

41 

42 

43def lines_to_path(lines: ArrayLike, index: NDArray[np.int64] | None = None) -> dict: 

44 """ 

45 Turn line segments into argument to be used for a Path2D or Path3D. 

46 

47 Parameters 

48 ------------ 

49 lines : (n, 2, dimension) or (n, dimension) float 

50 Line segments or connected polyline curve in 2D or 3D 

51 index : (n,) int64 

52 If passed save an index for each line segment. 

53 

54 Returns 

55 ----------- 

56 kwargs : dict 

57 kwargs for Path constructor 

58 """ 

59 lines = np.asanyarray(lines, dtype=np.float64) 

60 

61 if index is not None: 

62 index = np.asanyarray(index, dtype=np.int64) 

63 

64 if util.is_shape(lines, (-1, (2, 3))): 

65 # the case where we have a list of points 

66 # we are going to assume they are connected 

67 result = {"entities": np.array([Line(np.arange(len(lines)))]), "vertices": lines} 

68 return result 

69 elif util.is_shape(lines, (-1, 2, (2, 3))): 

70 # case where we have line segments in 2D or 3D 

71 dimension = lines.shape[-1] 

72 # convert lines to even number of (n, dimension) points 

73 lines = lines.reshape((-1, dimension)) 

74 # merge duplicate vertices 

75 unique, inverse = grouping.unique_rows(lines, digits=tol_path.merge_digits) 

76 # use scipy edges_to_path to skip creating 

77 # a bajillion individual line entities which 

78 # will be super slow vs. fewer polyline entities 

79 return edges_to_path(edges=inverse.reshape((-1, 2)), vertices=lines[unique]) 

80 else: 

81 raise ValueError("Lines must be (n,(2|3)) or (n,2,(2|3))") 

82 return result 

83 

84 

85def polygon_to_path(polygon): 

86 """ 

87 Load shapely Polygon objects into a trimesh.path.Path2D object 

88 

89 Parameters 

90 ------------- 

91 polygon : shapely.geometry.Polygon 

92 Input geometry 

93 

94 Returns 

95 ----------- 

96 kwargs : dict 

97 Keyword arguments for Path2D constructor 

98 """ 

99 # start with a single polyline for the exterior 

100 entities = [] 

101 # start vertices 

102 vertices = [] 

103 

104 if hasattr(polygon.boundary, "geoms"): 

105 boundaries = polygon.boundary.geoms 

106 else: 

107 boundaries = [polygon.boundary] 

108 

109 # append interiors as single Line objects 

110 current = 0 

111 for boundary in boundaries: 

112 entities.append(Line(np.arange(len(boundary.coords)) + current)) 

113 current += len(boundary.coords) 

114 # append the new vertex array 

115 vertices.append(np.array(boundary.coords)) 

116 

117 # make sure result arrays are numpy 

118 kwargs = { 

119 "entities": entities, 

120 "vertices": np.vstack(vertices) if len(vertices) > 0 else vertices, 

121 } 

122 

123 return kwargs 

124 

125 

126def linestrings_to_path(multi) -> dict: 

127 """ 

128 Load shapely LineString objects into arguments to create a Path2D or Path3D. 

129 

130 Parameters 

131 ------------- 

132 multi : shapely.geometry.LineString or MultiLineString 

133 Input 2D or 3D geometry 

134 

135 Returns 

136 ------------- 

137 kwargs : dict 

138 Keyword arguments for Path2D or Path3D constructor 

139 """ 

140 import shapely 

141 

142 # append to result as we go 

143 entities = [] 

144 vertices = [] 

145 

146 if isinstance(multi, shapely.MultiLineString): 

147 multi = list(multi.geoms) 

148 else: 

149 multi = [multi] 

150 

151 for line in multi: 

152 # only append geometry with points 

153 if hasattr(line, "coords"): 

154 coords = np.array(line.coords) 

155 if len(coords) < 2: 

156 continue 

157 entities.append(Line(np.arange(len(coords)) + len(vertices))) 

158 vertices.extend(coords) 

159 

160 kwargs = {"entities": np.array(entities), "vertices": np.array(vertices)} 

161 return kwargs 

162 

163 

164def faces_to_path(mesh, face_ids=None, **kwargs): 

165 """ 

166 Given a mesh and face indices find the outline edges and 

167 turn them into a Path3D. 

168 

169 Parameters 

170 ------------ 

171 mesh : trimesh.Trimesh 

172 Triangulated surface in 3D 

173 face_ids : (n,) int 

174 Indexes referencing mesh.faces 

175 

176 Returns 

177 --------- 

178 kwargs : dict 

179 Kwargs for Path3D constructor 

180 """ 

181 if face_ids is None: 

182 edges = mesh.edges_sorted 

183 else: 

184 # take advantage of edge ordering to index as single row 

185 edges = mesh.edges_sorted.reshape((-1, 6))[face_ids].reshape((-1, 2)) 

186 # an edge which occurs onely once is on the boundary 

187 unique_edges = grouping.group_rows(edges, require_count=1) 

188 # add edges and vertices to kwargs 

189 kwargs.update(edges_to_path(edges=edges[unique_edges], vertices=mesh.vertices)) 

190 

191 return kwargs 

192 

193 

194def edges_to_path(edges: ArrayLike, vertices: ArrayLike, **kwargs) -> dict: 

195 """ 

196 Given an edge list of indices and associated vertices 

197 representing lines, generate kwargs for a Path object. 

198 

199 Parameters 

200 ----------- 

201 edges : (n, 2) int 

202 Vertex indices of line segments 

203 vertices : (m, dimension) float 

204 Vertex positions where dimension is 2 or 3 

205 

206 Returns 

207 ---------- 

208 kwargs : dict 

209 Kwargs for Path constructor 

210 """ 

211 # sequence of ordered traversals 

212 dfs = graph.traversals(edges, mode="dfs") 

213 # make sure every consecutive index in DFS 

214 # traversal is an edge in the source edge list 

215 dfs_connected = graph.fill_traversals(dfs, edges=edges) 

216 # kwargs for Path constructor 

217 # turn traversals into Line objects 

218 lines = [Line(d) for d in dfs_connected] 

219 

220 kwargs.update({"entities": lines, "vertices": vertices, "process": False}) 

221 return kwargs