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
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1import numpy as np
3from ... import graph, grouping, util
4from ...constants import tol_path
5from ...typed import ArrayLike, NDArray
6from ..entities import Arc, Line
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.
14 Parameters
15 ------------
16 as_dict : dict
17 Has keys: 'vertices', 'entities'
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
40 return result
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.
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.
54 Returns
55 -----------
56 kwargs : dict
57 kwargs for Path constructor
58 """
59 lines = np.asanyarray(lines, dtype=np.float64)
61 if index is not None:
62 index = np.asanyarray(index, dtype=np.int64)
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
85def polygon_to_path(polygon):
86 """
87 Load shapely Polygon objects into a trimesh.path.Path2D object
89 Parameters
90 -------------
91 polygon : shapely.geometry.Polygon
92 Input geometry
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 = []
104 if hasattr(polygon.boundary, "geoms"):
105 boundaries = polygon.boundary.geoms
106 else:
107 boundaries = [polygon.boundary]
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))
117 # make sure result arrays are numpy
118 kwargs = {
119 "entities": entities,
120 "vertices": np.vstack(vertices) if len(vertices) > 0 else vertices,
121 }
123 return kwargs
126def linestrings_to_path(multi) -> dict:
127 """
128 Load shapely LineString objects into arguments to create a Path2D or Path3D.
130 Parameters
131 -------------
132 multi : shapely.geometry.LineString or MultiLineString
133 Input 2D or 3D geometry
135 Returns
136 -------------
137 kwargs : dict
138 Keyword arguments for Path2D or Path3D constructor
139 """
140 import shapely
142 # append to result as we go
143 entities = []
144 vertices = []
146 if isinstance(multi, shapely.MultiLineString):
147 multi = list(multi.geoms)
148 else:
149 multi = [multi]
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)
160 kwargs = {"entities": np.array(entities), "vertices": np.array(vertices)}
161 return kwargs
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.
169 Parameters
170 ------------
171 mesh : trimesh.Trimesh
172 Triangulated surface in 3D
173 face_ids : (n,) int
174 Indexes referencing mesh.faces
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))
191 return kwargs
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.
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
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]
220 kwargs.update({"entities": lines, "vertices": vertices, "process": False})
221 return kwargs