Coverage for trimesh/path/creation.py: 92%
95 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 transformations, util
4from ..geometry import plane_transform
5from . import arc
6from .entities import Arc, Line
9def circle_pattern(
10 pattern_radius, circle_radius, count, center=None, angle=None, **kwargs
11):
12 """
13 Create a Path2D representing a circle pattern.
15 Parameters
16 ------------
17 pattern_radius : float
18 Radius of circle centers
19 circle_radius : float
20 The radius of each circle
21 count : int
22 Number of circles in the pattern
23 center : (2,) float
24 Center of pattern
25 angle : float
26 If defined pattern will span this angle
27 If None, pattern will be evenly spaced
29 Returns
30 -------------
31 pattern : trimesh.path.Path2D
32 Path containing circular pattern
33 """
34 from .path import Path2D
36 if angle is None:
37 angles = np.linspace(0.0, np.pi * 2.0, count + 1)[:-1]
38 elif isinstance(angle, float) or isinstance(angle, int):
39 angles = np.linspace(0.0, angle, count)
40 else:
41 raise ValueError("angle must be float or int!")
43 if center is None:
44 center = [0.0, 0.0]
46 # centers of circles
47 centers = np.column_stack((np.cos(angles), np.sin(angles))) * pattern_radius
49 vert = []
50 ents = []
51 for circle_center in centers:
52 # (3,3) center points of arc
53 three = arc.to_threepoint(
54 angles=[0, np.pi], center=circle_center, radius=circle_radius
55 )
56 # add a single circle entity
57 ents.append(Arc(points=np.arange(3) + len(vert), closed=True))
58 # keep flat array by extend instead of append
59 vert.extend(three)
61 # translate vertices to pattern center
62 vert = np.array(vert) + center
63 pattern = Path2D(entities=ents, vertices=vert, **kwargs)
64 return pattern
67def circle(radius, center=None, **kwargs):
68 """
69 Create a Path2D containing circle with the specified
70 radius.
72 Parameters
73 --------------
74 radius : float
75 The radius of the circle
76 center : None or (2,) float
77 Center of the circle, origin by default
78 ** kwargs : dict
79 Passed to trimesh.path.Path2D constructor
81 Returns
82 -------------
83 circle : Path2D
84 Path containing specified circle
85 """
86 from .path import Path2D
88 if center is None:
89 center = [0.0, 0.0]
90 else:
91 center = np.asanyarray(center, dtype=np.float64)
92 # make sure radius is a float
93 radius = float(radius)
95 # (3, 2) float, points on arc
96 three = arc.to_threepoint(angles=[0, np.pi], center=center, radius=radius)
97 # generate the path object
98 result = Path2D(
99 entities=[Arc(points=np.arange(3), closed=True)], vertices=three, **kwargs
100 )
102 return result
105def rectangle(bounds, **kwargs):
106 """
107 Create a Path2D containing a single or multiple rectangles
108 with the specified bounds.
110 Parameters
111 --------------
112 bounds : (2, 2) float, or (m, 2, 2) float
113 Minimum XY, Maximum XY
115 Returns
116 -------------
117 rect : Path2D
118 Path containing specified rectangles
119 """
120 from .path import Path2D
122 # data should be float
123 bounds = np.asanyarray(bounds, dtype=np.float64)
125 # bounds are extents, re- shape to origin- centered rectangle
126 if bounds.shape == (2,):
127 half = np.abs(bounds) / 2.0
128 bounds = np.array([-half, half])
130 # should have one bounds or multiple bounds
131 if not (util.is_shape(bounds, (2, 2)) or util.is_shape(bounds, (-1, 2, 2))):
132 raise ValueError("bounds must be (m, 2, 2) or (2, 2)")
134 # hold Line objects
135 lines = []
136 # hold (n, 2) cartesian points
137 vertices = []
139 # loop through each rectangle
140 for lower, upper in bounds.reshape((-1, 2, 2)):
141 lines.append(Line((np.arange(5) % 4) + len(vertices)))
142 vertices.extend([lower, [upper[0], lower[1]], upper, [lower[0], upper[1]]])
144 # create the Path2D with specified rectangles
145 rect = Path2D(entities=lines, vertices=vertices, **kwargs)
147 return rect
150def box_outline(extents=None, transform=None, **kwargs):
151 """
152 Return a cuboid.
154 Parameters
155 ------------
156 extents : float, or (3,) float
157 Edge lengths
158 transform: (4, 4) float
159 Transformation matrix
160 **kwargs:
161 passed to Trimesh to create box
163 Returns
164 ------------
165 geometry : trimesh.Path3D
166 Path outline of a cuboid geometry
167 """
168 from .exchange.load import load_path
170 # create vertices for the box
171 vertices = [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1]
172 vertices = np.array(vertices, order="C", dtype=np.float64).reshape((-1, 3))
173 vertices -= 0.5
175 # resize the vertices based on passed size
176 if extents is not None:
177 extents = np.asanyarray(extents, dtype=np.float64)
178 if extents.shape != (3,):
179 raise ValueError("Extents must be (3,)!")
180 vertices *= extents
182 # apply transform if passed
183 if transform is not None:
184 vertices = transformations.transform_points(vertices, transform)
186 # vertex indices
187 indices = [0, 1, 3, 2, 0, 4, 5, 7, 6, 4, 0, 2, 6, 7, 3, 1, 5]
188 outline = load_path(vertices[indices])
190 return outline
193def grid(
194 side,
195 count=5,
196 transform=None,
197 plane_origin=None,
198 plane_normal=None,
199 include_circle=True,
200 sections_circle=32,
201):
202 """
203 Create a Path3D for a grid visualization of a plane.
205 Parameters
206 -----------
207 side : float
208 Length of half of a grid side
209 count : int
210 Number of grid lines per grid half
211 transform : None or (4, 4) float
212 Transformation matrix to move grid location.
213 Takes precedence over plane_origin if both are passed.
214 plane_origin : None or (3,) float
215 Plane origin
216 plane_normal : None or (3,) float
217 Unit normal vector
218 include_circle : bool
219 Include a circular pattern inside the grid
220 sections_circle : int
221 How many sections should the smallest circle have
223 Returns
224 ----------
225 grid : trimesh.path.Path3D
226 Path containing grid plane visualization
227 """
228 from .path import Path3D
230 # change full side length to half-side
231 side = float(side)
232 # make sure count is an integer
233 count = int(count)
234 # get a spaced sequence of radius
235 radii = np.linspace(0.0, side, count + 1)[1:]
236 # what's the maximum radius
237 rmax = radii[-1]
239 # keep a count of the current vertex count
240 current = 0
241 # collect vertices and entities
242 vertices = []
243 entities = []
244 for r in radii:
245 if include_circle:
246 # scale the section count by radius
247 circle_res = int((r / radii[0]) * sections_circle)
248 # generate a circule pattern
249 theta = np.linspace(0.0, np.pi * 2, circle_res)
250 circle = np.column_stack((np.cos(theta), np.sin(theta))) * r
251 # append the circle pattern
252 vertices.append(circle)
253 entities.append(Line(points=np.arange(len(circle)) + current))
254 # keep the vertex count correct
255 current += len(circle)
256 # generate a series of grid lines
257 vertices.append(
258 [
259 [-rmax, r],
260 [rmax, r],
261 [-rmax, -r],
262 [rmax, -r],
263 [r, -rmax],
264 [r, rmax],
265 [-r, -rmax],
266 [-r, rmax],
267 ]
268 )
269 # append an entity per grid line
270 for i in [0, 2, 4, 6]:
271 entities.append(Line(points=np.arange(2) + current + i))
272 current += len(vertices[-1])
274 # add the middle lines which were skipped
275 vertices.append([[0, rmax], [0, -rmax], [-rmax, 0], [rmax, 0]])
276 entities.append(Line(points=np.arange(2) + current))
277 entities.append(Line(points=np.arange(2) + current + 2))
278 # stack vertices into clean (n, 3) float
279 vertices = np.vstack(vertices)
281 # if plane was passed instead of transform create the matrix here
282 if transform is None and plane_origin is not None and plane_normal is not None:
283 transform = np.linalg.inv(
284 plane_transform(origin=plane_origin, normal=plane_normal)
285 )
287 # stack vertices to 3D
288 vertices = np.column_stack((vertices, np.zeros(len(vertices))))
289 # apply transform if passed
290 if transform is not None:
291 vertices = transformations.transform_points(vertices, matrix=transform)
292 # combine result into a Path3D object
293 grid_path = Path3D(entities=entities, vertices=vertices)
294 return grid_path