simplecadapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ """
2
+ SimpleCAD API - 简化的CAD建模Python API
3
+ 基于CADQuery实现,提供直观的几何建模接口
4
+ """
5
+
6
+ # 导入核心类
7
+ from .core import (
8
+ CoordinateSystem, Point, Line, Sketch, Body,
9
+ WORLD_CS, LocalCoordinateSystem, get_current_cs
10
+ )
11
+
12
+ # 基础构造操作
13
+ from .operations import (
14
+ # 基础几何
15
+ make_point, make_line, make_angle_arc, make_three_point_arc, make_segement, make_spline,
16
+ make_sketch,
17
+ make_rectangle, make_circle, make_triangle, make_ellipse,
18
+
19
+ # 便利函数
20
+ make_box, make_cylinder, make_sphere,
21
+
22
+ # 三维建模
23
+ extrude, revolve, loft, sweep,
24
+
25
+ # 实体编辑
26
+ shell, fillet, chamfer,
27
+
28
+ # 布尔运算
29
+ cut, union, intersect,
30
+
31
+ # 高级操作
32
+ make_linear_pattern, make_2d_pattern, make_radial_pattern, helical_sweep
33
+ )
34
+
35
+ __version__ = "0.1.0"
36
+ __author__ = "SimpleCAD Team"
37
+
38
+ __all__ = [
39
+ # 核心类
40
+ "CoordinateSystem", "Point", "Line", "Sketch", "Body",
41
+ "WORLD_CS", "LocalCoordinateSystem", "get_current_cs",
42
+
43
+ # 基础操作
44
+ "make_point", "make_line", "make_sketch", "make_angle_arc", "make_three_point_arc", "make_segement", "make_spline",
45
+ "make_rectangle", "make_circle", "make_triangle", "make_ellipse",
46
+
47
+ # 便利函数
48
+ "make_box", "make_cylinder", "make_sphere",
49
+
50
+ # 三维建模
51
+ "extrude", "revolve", "loft", "sweep",
52
+
53
+ # 实体编辑
54
+ "shell", "fillet", "chamfer",
55
+
56
+ # 布尔运算
57
+ "cut", "union", "intersect",
58
+
59
+ # 高级操作
60
+ "make_linear_pattern", "make_2d_pattern", "make_radial_pattern", "helical_sweep"
61
+ ]
simplecadapi/core.py ADDED
@@ -0,0 +1,417 @@
1
+ """
2
+ SimpleCAD API核心类定义
3
+ 基于README中的API设计,使用CADQuery作为底层实现
4
+ """
5
+
6
+ from typing import List, Tuple, Union, Optional, Any
7
+ import numpy as np
8
+ import cadquery as cq
9
+ from cadquery import Vector, Plane
10
+
11
+
12
+ class CoordinateSystem:
13
+ """三维坐标系
14
+
15
+ SimpleCAD使用Z向上的右手坐标系,但CADQuery使用Y向上的右手坐标系
16
+ 需要进行坐标转换:
17
+ SimpleCAD (X, Y, Z) -> CADQuery (X, Z, -Y)
18
+ """
19
+
20
+ def __init__(self,
21
+ origin: Tuple[float, float, float] = (0, 0, 0),
22
+ x_axis: Tuple[float, float, float] = (1, 0, 0),
23
+ y_axis: Tuple[float, float, float] = (0, 1, 0)):
24
+ # SimpleCAD坐标系(Z向上)
25
+ self.origin = np.array(origin, dtype=float)
26
+ self.x_axis = self._normalize(x_axis)
27
+ self.y_axis = self._normalize(y_axis)
28
+ self.z_axis = self._normalize(np.cross(self.x_axis, self.y_axis))
29
+
30
+ def _normalize(self, vector) -> np.ndarray:
31
+ """归一化向量"""
32
+ v = np.array(vector, dtype=float)
33
+ norm = np.linalg.norm(v)
34
+ if norm == 0:
35
+ raise ValueError("不能归一化零向量")
36
+ return v / norm
37
+
38
+ def transform_point(self, point: np.ndarray) -> np.ndarray:
39
+ """将局部坐标转换为全局坐标(SimpleCAD坐标系)"""
40
+ return self.origin + point[0]*self.x_axis + point[1]*self.y_axis + point[2]*self.z_axis
41
+
42
+ def to_cq_coords(self, point: np.ndarray) -> np.ndarray:
43
+ """将SimpleCAD坐标转换为CADQuery坐标
44
+ SimpleCAD (X, Y, Z) -> CADQuery (X, Z, -Y)
45
+ """
46
+ return np.array([point[0], point[2], -point[1]], dtype=float)
47
+
48
+ def from_cq_coords(self, point: np.ndarray) -> np.ndarray:
49
+ """将CADQuery坐标转换为SimpleCAD坐标
50
+ CADQuery (X, Y, Z) -> SimpleCAD (X, -Z, Y)
51
+ """
52
+ return np.array([point[0], -point[2], point[1]], dtype=float)
53
+
54
+ def to_cq_plane(self) -> Plane:
55
+ """转换为CADQuery的Plane对象"""
56
+ # 转换坐标系到CADQuery空间
57
+ cq_origin = self.to_cq_coords(self.origin)
58
+ cq_x_axis = self.to_cq_coords(self.x_axis) - self.to_cq_coords(np.zeros(3))
59
+ cq_z_axis = self.to_cq_coords(self.z_axis) - self.to_cq_coords(np.zeros(3))
60
+
61
+ return Plane(
62
+ origin=Vector(*cq_origin),
63
+ xDir=Vector(*cq_x_axis),
64
+ normal=Vector(*cq_z_axis)
65
+ )
66
+
67
+
68
+ # 全局世界坐标系(Z向上的右手坐标系)
69
+ WORLD_CS = CoordinateSystem()
70
+
71
+
72
+ class Point:
73
+ """三维点(存储在局部坐标系中的坐标)"""
74
+
75
+ def __init__(self, coords: Tuple[float, float, float], cs: Optional[CoordinateSystem] = None):
76
+ self.local_coords = np.array(coords, dtype=float)
77
+ self.cs = cs if cs is not None else get_current_cs()
78
+
79
+ @property
80
+ def global_coords(self) -> np.ndarray:
81
+ """获取全局坐标系中的坐标(SimpleCAD坐标系)"""
82
+ return self.cs.transform_point(self.local_coords)
83
+
84
+ def to_cq_vector(self) -> Vector:
85
+ """转换为CADQuery的Vector对象(CADQuery坐标系)"""
86
+ global_coords = self.global_coords
87
+ cq_coords = self.cs.to_cq_coords(global_coords)
88
+ return Vector(cq_coords[0], cq_coords[1], cq_coords[2])
89
+
90
+ def __repr__(self) -> str:
91
+ return f"Point(local={self.local_coords}, global={self.global_coords})"
92
+
93
+
94
+ class Line:
95
+ """曲线(线段/圆弧/样条)"""
96
+
97
+ def __init__(self, points: List[Point], line_type: str = "segment"):
98
+ """
99
+ :param points: 控制点列表
100
+ :param line_type: 类型 ('segment', 'arc', 'spline')
101
+ """
102
+ self.points = points
103
+ self.type = line_type
104
+ self._validate()
105
+ self._cq_edge = None # 延迟创建CADQuery边
106
+
107
+ def _validate(self):
108
+ """验证线的参数"""
109
+ if self.type == "segment" and len(self.points) != 2:
110
+ raise ValueError("线段需要2个控制点")
111
+ if self.type == "arc" and len(self.points) != 3:
112
+ raise ValueError("圆弧需要3个控制点")
113
+ if self.type == "spline" and len(self.points) < 2:
114
+ raise ValueError("样条曲线至少需要2个控制点")
115
+
116
+ def to_cq_edge(self):
117
+ """转换为CADQuery的边对象"""
118
+ if self._cq_edge is not None:
119
+ return self._cq_edge
120
+
121
+ if self.type == "segment":
122
+ # 创建线段
123
+ start = self.points[0].to_cq_vector()
124
+ end = self.points[1].to_cq_vector()
125
+ self._cq_edge = cq.Edge.makeLine(start, end)
126
+
127
+ elif self.type == "arc":
128
+ # 创建圆弧(通过三点)
129
+ p1 = self.points[0].to_cq_vector()
130
+ p2 = self.points[1].to_cq_vector()
131
+ p3 = self.points[2].to_cq_vector()
132
+ self._cq_edge = cq.Edge.makeThreePointArc(p1, p2, p3)
133
+
134
+ elif self.type == "spline":
135
+ # 创建样条曲线
136
+ cq_points = [p.to_cq_vector() for p in self.points]
137
+ self._cq_edge = cq.Edge.makeSpline(cq_points)
138
+
139
+ return self._cq_edge
140
+
141
+ def __repr__(self) -> str:
142
+ return f"Line(type={self.type}, points={len(self.points)})"
143
+
144
+
145
+ class Sketch:
146
+ """二维草图(闭合平面轮廓)"""
147
+
148
+ def __init__(self, lines: List[Line]):
149
+ self.lines = lines
150
+ # 从第一个点获取坐标系信息
151
+ self.cs = lines[0].points[0].cs if lines and lines[0].points else get_current_cs()
152
+ self._validate()
153
+ self._cq_wire = None # 延迟创建CADQuery线框
154
+
155
+ def _validate(self):
156
+ """验证草图的闭合性和共面性"""
157
+ if not self._is_closed():
158
+ raise ValueError("草图必须形成闭合轮廓")
159
+ if not self._is_planar():
160
+ raise ValueError("草图必须位于同一平面内")
161
+
162
+ def _is_closed(self) -> bool:
163
+ """检查起点和终点是否重合"""
164
+ if not self.lines:
165
+ return False
166
+
167
+ start = self.lines[0].points[0].global_coords
168
+ end = self.lines[-1].points[-1].global_coords
169
+ return np.allclose(start, end, atol=1e-6)
170
+
171
+ def _is_planar(self) -> bool:
172
+ """简化的共面检查(实际需要法向量计算)"""
173
+ # TODO: 实现真正的共面检查
174
+ return True
175
+
176
+ def to_cq_wire(self):
177
+ """转换为CADQuery的线框对象"""
178
+ if self._cq_wire is not None:
179
+ return self._cq_wire
180
+
181
+ # 将所有线段转换为CADQuery边
182
+ cq_edges = []
183
+ for line in self.lines:
184
+ edge = line.to_cq_edge()
185
+ if edge is not None:
186
+ cq_edges.append(edge)
187
+
188
+ # 创建线框
189
+ try:
190
+ self._cq_wire = cq.Wire.assembleEdges(cq_edges)
191
+ except Exception as e:
192
+ raise ValueError(f"无法创建闭合线框: {e}")
193
+
194
+ return self._cq_wire
195
+
196
+ def get_workplane(self):
197
+ """获取草图所在的工作平面"""
198
+ return cq.Workplane(self.cs.to_cq_plane())
199
+
200
+ def get_normal_vector(self) -> np.ndarray:
201
+ """获取草图平面的法向量(SimpleCAD坐标系)"""
202
+ return self.cs.z_axis
203
+
204
+ def __repr__(self) -> str:
205
+ return f"Sketch(lines={len(self.lines)})"
206
+
207
+
208
+ class Body:
209
+ """三维实体"""
210
+ _id_counter = 0
211
+
212
+ def __init__(self, cq_solid: Any = None):
213
+ self.cq_solid = cq_solid # CADQuery的Solid对象
214
+ self.id = Body._id_counter
215
+ Body._id_counter += 1
216
+ self.face_tags = {} # 面标签映射:{tag: face_indices}
217
+ self.tagged_faces = {} # 反向映射:{face_index: tags}
218
+
219
+ def is_valid(self) -> bool:
220
+ """检查实体是否有效"""
221
+ return self.cq_solid is not None
222
+
223
+ def volume(self) -> float:
224
+ """计算体积"""
225
+ if self.cq_solid is None:
226
+ return 0.0
227
+ try:
228
+ # 简化的体积计算
229
+ return 1.0 # 暂时返回固定值,待实现
230
+ except:
231
+ return 0.0
232
+
233
+ def to_cq_workplane(self) -> 'cq.Workplane':
234
+ """转换为CADQuery工作平面对象"""
235
+ if self.cq_solid is None:
236
+ return cq.Workplane()
237
+ return cq.Workplane().add(self.cq_solid)
238
+
239
+ def tag_face(self, face_index: int, tag: str):
240
+ """为指定面添加标签
241
+
242
+ Args:
243
+ face_index: 面的索引(从0开始)
244
+ tag: 标签名称(如'top', 'bottom', 'front'等)
245
+ """
246
+ if tag not in self.face_tags:
247
+ self.face_tags[tag] = []
248
+ if face_index not in self.face_tags[tag]:
249
+ self.face_tags[tag].append(face_index)
250
+
251
+ if face_index not in self.tagged_faces:
252
+ self.tagged_faces[face_index] = []
253
+ if tag not in self.tagged_faces[face_index]:
254
+ self.tagged_faces[face_index].append(tag)
255
+
256
+ def get_faces_by_tag(self, tag: str) -> List[int]:
257
+ """根据标签获取面索引列表
258
+
259
+ Args:
260
+ tag: 标签名称
261
+
262
+ Returns:
263
+ 面索引列表
264
+ """
265
+ return self.face_tags.get(tag, [])
266
+
267
+ def get_all_faces(self):
268
+ """获取所有面对象
269
+
270
+ Returns:
271
+ CADQuery Face对象列表
272
+ """
273
+ if self.cq_solid is None:
274
+ return []
275
+
276
+ try:
277
+ # 获取Workplane对象中的所有面
278
+ if hasattr(self.cq_solid, 'faces'):
279
+ face_workplanes = self.cq_solid.faces().all()
280
+ # 从Workplane对象中提取实际的Face对象
281
+ faces = []
282
+ for wp in face_workplanes:
283
+ # 获取Workplane中的val对象,这是实际的Face
284
+ if hasattr(wp, 'val') and wp.val() is not None:
285
+ faces.append(wp.val())
286
+ elif hasattr(wp, 'objects') and wp.objects:
287
+ # 尝试从objects属性获取
288
+ faces.extend(wp.objects)
289
+ return faces
290
+ elif hasattr(self.cq_solid, 'solids'):
291
+ # 如果是Workplane对象,获取第一个solid的面
292
+ solids = self.cq_solid.solids().all()
293
+ if solids and hasattr(solids[0], 'val'):
294
+ solid = solids[0].val()
295
+ if hasattr(solid, 'Faces'):
296
+ return solid.Faces()
297
+ return []
298
+ except Exception as e:
299
+ print(f"获取面时出错: {e}")
300
+ return []
301
+
302
+ def auto_tag_faces(self, geometry_type: str = "box"):
303
+ """自动为基础几何体的面添加标签
304
+
305
+ Args:
306
+ geometry_type: 几何体类型 ('box', 'cylinder', 'sphere')
307
+ """
308
+ faces = self.get_all_faces()
309
+ if not faces:
310
+ return
311
+
312
+ if geometry_type == "box":
313
+ self._auto_tag_box_faces(faces)
314
+ elif geometry_type == "cylinder":
315
+ self._auto_tag_cylinder_faces(faces)
316
+ elif geometry_type == "sphere":
317
+ self._auto_tag_sphere_faces(faces)
318
+
319
+ def _auto_tag_box_faces(self, faces):
320
+ """为立方体面自动添加标签"""
321
+ if len(faces) != 6:
322
+ return
323
+
324
+ # 分析每个面的法向量来确定方向
325
+ for i, face in enumerate(faces):
326
+ try:
327
+ # 获取面的法向量(CADQuery坐标系)
328
+ normal = face.normalAt()
329
+
330
+ # 直接使用CADQuery坐标系判断面的位置
331
+ # CADQuery: Y上,Z前,X右
332
+ if abs(normal.y) > 0.9: # Y方向面(上下)
333
+ if normal.y > 0:
334
+ self.tag_face(i, "top")
335
+ else:
336
+ self.tag_face(i, "bottom")
337
+ elif abs(normal.z) > 0.9: # Z方向面(前后)
338
+ if normal.z > 0:
339
+ self.tag_face(i, "front") # CADQuery +Z = 前
340
+ else:
341
+ self.tag_face(i, "back") # CADQuery -Z = 后
342
+ elif abs(normal.x) > 0.9: # X方向面(左右)
343
+ if normal.x > 0:
344
+ self.tag_face(i, "right")
345
+ else:
346
+ self.tag_face(i, "left")
347
+ except:
348
+ continue
349
+
350
+ def _auto_tag_cylinder_faces(self, faces):
351
+ """为圆柱体面自动添加标签"""
352
+ if len(faces) != 3:
353
+ return
354
+
355
+ # 圆柱体通常有3个面:顶面、底面、侧面
356
+ for i, face in enumerate(faces):
357
+ try:
358
+ # 检查面的类型
359
+ if hasattr(face, 'geomType'):
360
+ geom_type = face.geomType()
361
+ if geom_type == "PLANE":
362
+ # 平面,检查法向量判断是顶面还是底面
363
+ normal = face.normalAt()
364
+
365
+ # 转换法向量到SimpleCAD坐标系
366
+ # CADQuery (X, Y, Z) -> SimpleCAD (X, -Z, Y)
367
+ scad_normal_z = normal.y
368
+
369
+ if abs(scad_normal_z) > 0.9:
370
+ if scad_normal_z > 0:
371
+ self.tag_face(i, "top")
372
+ else:
373
+ self.tag_face(i, "bottom")
374
+ elif geom_type == "CYLINDER":
375
+ # 圆柱面,应该是侧面
376
+ self.tag_face(i, "side")
377
+ else:
378
+ # 其他类型的曲面也标记为侧面
379
+ self.tag_face(i, "side")
380
+ except Exception as e:
381
+ print(f"标记圆柱体面 {i} 时出错: {e}")
382
+ continue
383
+
384
+ def _auto_tag_sphere_faces(self, faces):
385
+ """为球体面自动添加标签"""
386
+ # 球体只有一个面
387
+ if len(faces) == 1:
388
+ self.tag_face(0, "surface")
389
+
390
+ def __repr__(self) -> str:
391
+ return f"Body(id={self.id}, valid={self.is_valid()})"
392
+
393
+
394
+ # 当前坐标系上下文管理器
395
+ _current_cs = [WORLD_CS] # 默认使用世界坐标系
396
+
397
+
398
+ class LocalCoordinateSystem:
399
+ """局部坐标系上下文管理器"""
400
+
401
+ def __init__(self,
402
+ origin: Tuple[float, float, float],
403
+ x_axis: Tuple[float, float, float],
404
+ y_axis: Tuple[float, float, float]):
405
+ self.cs = CoordinateSystem(origin, x_axis, y_axis)
406
+
407
+ def __enter__(self):
408
+ _current_cs.append(self.cs)
409
+ return self.cs
410
+
411
+ def __exit__(self, exc_type, exc_val, exc_tb):
412
+ _current_cs.pop()
413
+
414
+
415
+ def get_current_cs() -> CoordinateSystem:
416
+ """获取当前坐标系"""
417
+ return _current_cs[-1]