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

1import numpy as np 

2 

3from .. import transformations, util 

4from ..geometry import plane_transform 

5from . import arc 

6from .entities import Arc, Line 

7 

8 

9def circle_pattern( 

10 pattern_radius, circle_radius, count, center=None, angle=None, **kwargs 

11): 

12 """ 

13 Create a Path2D representing a circle pattern. 

14 

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 

28 

29 Returns 

30 ------------- 

31 pattern : trimesh.path.Path2D 

32 Path containing circular pattern 

33 """ 

34 from .path import Path2D 

35 

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

42 

43 if center is None: 

44 center = [0.0, 0.0] 

45 

46 # centers of circles 

47 centers = np.column_stack((np.cos(angles), np.sin(angles))) * pattern_radius 

48 

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) 

60 

61 # translate vertices to pattern center 

62 vert = np.array(vert) + center 

63 pattern = Path2D(entities=ents, vertices=vert, **kwargs) 

64 return pattern 

65 

66 

67def circle(radius, center=None, **kwargs): 

68 """ 

69 Create a Path2D containing circle with the specified 

70 radius. 

71 

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 

80 

81 Returns 

82 ------------- 

83 circle : Path2D 

84 Path containing specified circle 

85 """ 

86 from .path import Path2D 

87 

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) 

94 

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 ) 

101 

102 return result 

103 

104 

105def rectangle(bounds, **kwargs): 

106 """ 

107 Create a Path2D containing a single or multiple rectangles 

108 with the specified bounds. 

109 

110 Parameters 

111 -------------- 

112 bounds : (2, 2) float, or (m, 2, 2) float 

113 Minimum XY, Maximum XY 

114 

115 Returns 

116 ------------- 

117 rect : Path2D 

118 Path containing specified rectangles 

119 """ 

120 from .path import Path2D 

121 

122 # data should be float 

123 bounds = np.asanyarray(bounds, dtype=np.float64) 

124 

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

129 

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

133 

134 # hold Line objects 

135 lines = [] 

136 # hold (n, 2) cartesian points 

137 vertices = [] 

138 

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

143 

144 # create the Path2D with specified rectangles 

145 rect = Path2D(entities=lines, vertices=vertices, **kwargs) 

146 

147 return rect 

148 

149 

150def box_outline(extents=None, transform=None, **kwargs): 

151 """ 

152 Return a cuboid. 

153 

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 

162 

163 Returns 

164 ------------ 

165 geometry : trimesh.Path3D 

166 Path outline of a cuboid geometry 

167 """ 

168 from .exchange.load import load_path 

169 

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 

174 

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 

181 

182 # apply transform if passed 

183 if transform is not None: 

184 vertices = transformations.transform_points(vertices, transform) 

185 

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

189 

190 return outline 

191 

192 

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. 

204 

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 

222 

223 Returns 

224 ---------- 

225 grid : trimesh.path.Path3D 

226 Path containing grid plane visualization 

227 """ 

228 from .path import Path3D 

229 

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] 

238 

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

273 

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) 

280 

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 ) 

286 

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