igniscad 0.1.0__tar.gz

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,25 @@
1
+ Metadata-Version: 2.3
2
+ Name: igniscad
3
+ Version: 0.1.0
4
+ Summary: A wrapper for the build123d library, designed for AI agents.
5
+ Author: CreeperIsASpy, Ext0gu1sher
6
+ Author-email: CreeperIsASpy <creeperspy@qq.com>, Ext0gu1sher <danielhuang070213@qq.com>
7
+ Requires-Dist: build123d<0.10
8
+ Requires-Dist: ocp-vscode>=3.0.1
9
+ Requires-Dist: yacv-server>=0.9.7
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+
13
+ > [!TIP]
14
+ > ***This project is one of the THOUGHT OF THE DAY projects.***
15
+ > ***该项目是“每日灵感记录”系列项目之一。***
16
+ > *The development of these project may not be active.*
17
+ > *这些项目的开发可能不会活跃。*
18
+
19
+ ## IgnisCAD
20
+
21
+ 使用 Python 封装 build123d 进行 CAD 开发,为 AI 建模打造。
22
+
23
+ LICENSE:Apache License 2.0
24
+
25
+ 欢迎使用。
@@ -0,0 +1,13 @@
1
+ > [!TIP]
2
+ > ***This project is one of the THOUGHT OF THE DAY projects.***
3
+ > ***该项目是“每日灵感记录”系列项目之一。***
4
+ > *The development of these project may not be active.*
5
+ > *这些项目的开发可能不会活跃。*
6
+
7
+ ## IgnisCAD
8
+
9
+ 使用 Python 封装 build123d 进行 CAD 开发,为 AI 建模打造。
10
+
11
+ LICENSE:Apache License 2.0
12
+
13
+ 欢迎使用。
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "igniscad"
3
+ version = "0.1.0"
4
+ description = "A wrapper for the build123d library, designed for AI agents."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "CreeperIsASpy", email = "creeperspy@qq.com" },
8
+ { name = "Ext0gu1sher", email = "danielhuang070213@qq.com" }
9
+ ]
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "build123d<0.10",
13
+ "ocp-vscode>=3.0.1",
14
+ "yacv-server>=0.9.7",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.9.24,<0.10.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [[tool.uv.index]]
22
+ name = "tuna"
23
+ url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from igniscad!"
@@ -0,0 +1,325 @@
1
+ import build123d as bd
2
+ import socketserver, webbrowser
3
+ import sys
4
+
5
+ # 1. 保存原始的错误处理方法,以免误伤其他真正的报错
6
+ _original_handle_error = socketserver.BaseServer.handle_error
7
+
8
+
9
+ def _silent_handle_error(self, request, client_address):
10
+ """
11
+ 自定义的错误处理器:忽略连接中断错误,其他错误照常打印。
12
+ """
13
+ # 获取刚刚发生的异常
14
+ exc_type, exc_value, _ = sys.exc_info()
15
+
16
+ # 检查是否是 WinError 10053 (ConnectionAbortedError)
17
+ # 或者是 BrokenPipeError (Linux/Mac 上常见的类似错误)
18
+ if isinstance(exc_value, (ConnectionAbortedError, BrokenPipeError)):
19
+ return # 直接忽略,不打印任何东西
20
+
21
+ # 如果是其他异常,调用原始方法(打印堆栈跟踪)
22
+ _original_handle_error(self, request, client_address)
23
+
24
+
25
+ # 2. 应用补丁:替换标准库的错误处理方法
26
+ socketserver.BaseServer.handle_error = _silent_handle_error
27
+ _GLOBAL_LAST_PART = None
28
+
29
+
30
+ class ContextManager:
31
+ """
32
+ 上下文管理器,负责捕获生成的模型。
33
+ """
34
+
35
+ def __init__(self, name):
36
+ self.name = name
37
+ self.part = None
38
+ self.registry = {}
39
+
40
+ def __enter__(self):
41
+ return self
42
+
43
+ def __exit__(self, exc_type, exc_val, exc_tb):
44
+ # 退出 with 块时,把当前零件保存到全局变量,供 show() 使用
45
+ global _GLOBAL_LAST_PART
46
+ if self.part:
47
+ self.part.label = self.name # 给零件打上标签
48
+ _GLOBAL_LAST_PART = self.part
49
+ return False
50
+
51
+ def __lshift__(self, other):
52
+ """
53
+ 重载 '<<' 操作符。
54
+ 用法: item << Cylinder(...) - Box(...)
55
+ """
56
+ if isinstance(other, Entity):
57
+ self.part = self.part + other if self.part else other
58
+ if other.name:
59
+ self.registry[other.name] = other
60
+ return self
61
+
62
+ def find(self, name):
63
+ return self.f(name)
64
+
65
+ def f(self, name):
66
+ if name in self.registry:
67
+ return self.registry[name]
68
+ raise ValueError(f"❌ Part '{name}' not found in this item.")
69
+
70
+
71
+ # --- 2. 实体封装 (Entity) ---
72
+ class Entity:
73
+ def __init__(self, part: bd.Part, name=None):
74
+ self.part = part
75
+ self.name = name
76
+
77
+ # --- 变换逻辑 ---
78
+ def move(self, x=0, y=0, z=0):
79
+ return Entity(self.part.moved(bd.Location((x, y, z))), self.name)
80
+
81
+ def rotate(self, x=0, y=0, z=0):
82
+ # 链式旋转
83
+ p = self.part
84
+ if x: p = p.rotate(bd.Axis.X, x)
85
+ if y: p = p.rotate(bd.Axis.Y, y)
86
+ if z: p = p.rotate(bd.Axis.Z, z)
87
+ return Entity(p, self.name)
88
+
89
+ # --- 布尔运算 ---
90
+ def _wrap_result(self, res):
91
+ """内部辅助:强制打包结果为 Compound"""
92
+ if not isinstance(res, (bd.Compound, bd.Solid)):
93
+ res = bd.Compound(res)
94
+ return res
95
+
96
+ def __sub__(self, other):
97
+ return self._wrap_result(self.part - other.part)
98
+
99
+ def __add__(self, other):
100
+ return self._wrap_result(self.part + other.part)
101
+
102
+ def __and__(self, other):
103
+ return self._wrap_result(self.part & other.part)
104
+
105
+ # --- 4. 尺寸与语义感知 (新增) ---
106
+ @property
107
+ def bbox(self):
108
+ """获取包围盒,方便计算相对位置"""
109
+ return self.part.bounding_box()
110
+
111
+ @property
112
+ def top(self):
113
+ """获取顶面中心点 Z 坐标"""
114
+ return self.bbox.max.Z
115
+
116
+ @property
117
+ def right(self):
118
+ """获取最右侧 X 坐标"""
119
+ return self.bbox.max.X
120
+
121
+ @property
122
+ def radius(self):
123
+ """尝试估算半径 (仅针对圆柱/圆球等)"""
124
+ # 简单逻辑:X 方向宽度的一半
125
+ return self.bbox.size.X / 2
126
+
127
+ # --- 5. 通用语义对齐 (Universal Align) ---
128
+ def align(self, target, face="top", offset=0):
129
+ """
130
+ 语义化对齐:将当前物体“吸附”到目标物体的指定面上。
131
+ 原理:计算目标面的中心,加上自身一半的厚度,实现无缝堆叠。
132
+
133
+ Args:
134
+ target (Entity): 目标物体
135
+ face (str): "top", "bottom", "left", "right", "front", "back"
136
+ offset (float): 额外的间隙(正数=远离,负数=嵌入)
137
+ """
138
+ if not isinstance(target, Entity):
139
+ raise ValueError("Target must be an Entity")
140
+
141
+ # 1. 获取包围盒信息
142
+ t_box = target.bbox # Target Bounding Box
143
+ s_box = self.bbox # Self Bounding Box (Current)
144
+
145
+ # 2. 默认目标位置为 Target 的中心点
146
+ # (如果不修改某轴坐标,默认就是中心对齐)
147
+ dest_x = t_box.center().X
148
+ dest_y = t_box.center().Y
149
+ dest_z = t_box.center().Z
150
+
151
+ # 3. 自身尺寸 (宽、深、高)
152
+ s_w = s_box.size.X
153
+ s_d = s_box.size.Y
154
+ s_h = s_box.size.Z
155
+
156
+ # 4. 根据 Face 修改目标坐标
157
+ # 逻辑:目标面坐标 +/- 自身一半尺寸 +/- 额外偏移
158
+ f = face.lower()
159
+
160
+ # Z轴 (上下)
161
+ if f == "top":
162
+ dest_z = t_box.max.Z + (s_h / 2) + offset
163
+ elif f == "bottom":
164
+ dest_z = t_box.min.Z - (s_h / 2) - offset
165
+
166
+ # X轴 (左右)
167
+ elif f == "right":
168
+ dest_x = t_box.max.X + (s_w / 2) + offset
169
+ elif f == "left":
170
+ dest_x = t_box.min.X - (s_w / 2) - offset
171
+
172
+ # Y轴 (前后) - 注意:CAD中通常 Y+ 是后(Back/North), Y- 是前(Front/South)
173
+ elif f == "back":
174
+ dest_y = t_box.max.Y + (s_d / 2) + offset
175
+ elif f == "front":
176
+ dest_y = t_box.min.Y - (s_d / 2) - offset
177
+ else:
178
+ raise ValueError(f"Unknown face: {face}. Use top/bottom/left/right/front/back")
179
+
180
+ # 5. 计算位移向量 (目标中心 - 当前中心)
181
+ # 这一步至关重要,因为物体当前不一定在原点
182
+ curr_x = s_box.center().X
183
+ curr_y = s_box.center().Y
184
+ curr_z = s_box.center().Z
185
+
186
+ dx = dest_x - curr_x
187
+ dy = dest_y - curr_y
188
+ dz = dest_z - curr_z
189
+
190
+ return self.move(dx, dy, dz)
191
+
192
+ # --- 6. 语义糖 (Syntactic Sugar for AI) ---
193
+ # 让 AI 写出类似自然语言的代码
194
+
195
+ def on_top_of(self, target, offset=0):
196
+ return self.align(target, "top", offset)
197
+
198
+ def under(self, target, offset=0):
199
+ return self.align(target, "bottom", offset)
200
+
201
+ def right_of(self, target, offset=0):
202
+ return self.align(target, "right", offset)
203
+
204
+ def left_of(self, target, offset=0):
205
+ return self.align(target, "left", offset)
206
+
207
+ def in_front_of(self, target, offset=0):
208
+ return self.align(target, "front", offset)
209
+
210
+ def behind(self, target, offset=0):
211
+ return self.align(target, "back", offset)
212
+
213
+
214
+ # --- 3. 顶层 API 函数 ---
215
+
216
+ def Item(name):
217
+ """创建上下文的工厂函数"""
218
+ return ContextManager(name)
219
+
220
+
221
+ def show():
222
+ """
223
+ 尝试连接 Yet Another CAD Viewer (浏览器),
224
+ 如果失败(未安装或报错),则直接保存并打开 STL。
225
+ """
226
+ global _GLOBAL_LAST_PART
227
+ if not _GLOBAL_LAST_PART:
228
+ print("⚠️ Nothing to show! (Did you forget to use 'item << ...'?)")
229
+ return
230
+
231
+ label = _GLOBAL_LAST_PART.label or "Model"
232
+ print(f"👀 Processing: {label}")
233
+
234
+ # 1. 尝试连接 Yet Another CAD Viewer (YACV)
235
+ try:
236
+ from yacv_server import show as yacv_show
237
+ target_obj = _GLOBAL_LAST_PART
238
+ yacv_show(target_obj, names=[label])
239
+
240
+ url = "http://localhost:32323"
241
+ print(f"✅ Sent to YACV (Check your browser, at {url})")
242
+ webbrowser.open(url)
243
+ input("✅ Press Enter to exit...")
244
+ return
245
+ except Exception as e:
246
+ print(f"⚠️ Failed to connect to YACV: {e}")
247
+
248
+ # 2. 如果上面失败了,直接导出 STL 文件
249
+ print("⚠️ Viewer not available. Exporting to disk...")
250
+
251
+ # 导出 STL
252
+ filename = f"{label}.stl"
253
+
254
+ try:
255
+ bd.export_stl(_GLOBAL_LAST_PART, filename)
256
+ except NameError:
257
+ import build123d as bd_fallback
258
+ bd_fallback.export_stl(_GLOBAL_LAST_PART, filename)
259
+
260
+ import os
261
+ abs_path = os.path.abspath(filename)
262
+ print(f"💾 Saved: {abs_path}")
263
+ print("👉 You can open this file with Windows 3D Viewer.")
264
+
265
+ try:
266
+ os.startfile(abs_path)
267
+ except:
268
+ pass
269
+
270
+ # --- 2. 原语工厂 (Primitives) ---
271
+ # AI 只需要调用这些简单的函数,不需要处理复杂的 build123d 参数
272
+
273
+ def Box(x, y, z, name=None) -> Entity:
274
+ # 默认居中对齐,方便 AI 这种直觉动物
275
+ return Entity(bd.Part(bd.Box(x, y, z, align=(bd.Align.CENTER, bd.Align.CENTER, bd.Align.CENTER))), name)
276
+
277
+ def Cylinder(r, h, name=None) -> Entity:
278
+ return Entity(bd.Part(bd.Cylinder(radius=r, height=h, align=(bd.Align.CENTER, bd.Align.CENTER, bd.Align.CENTER))), name)
279
+
280
+ def Sphere(r, name=None) -> Entity:
281
+ return Entity(bd.Part(bd.Sphere(radius=r)), name)
282
+
283
+ def Torus(major, minor, name=None) -> Entity:
284
+ return Entity(bd.Part(bd.Torus(major_radius=major, minor_radius=minor)), name)
285
+
286
+
287
+ # --- 3. 上下文管理器 (Contexts) ---
288
+ # 用于处理“一组物体”的关系,避免重复写坐标计算
289
+
290
+ class Group(Entity):
291
+ """
292
+ 逻辑分组:将多个实体组合成一个单一实体。
293
+ 支持上下文管理器语法,组内的物体会自动进行布尔并集 (Union)。
294
+ 一旦退出 with 块,该 Group 对象即可像普通 Entity 一样被移动或对齐。
295
+ """
296
+
297
+ def __init__(self, name=None):
298
+ # 初始化时没有内部零件,设为 None
299
+ # 注意:在添加第一个零件前调用 move/rotate 会报错,这是预期的行为
300
+ super().__init__(None, name)
301
+
302
+ def __enter__(self):
303
+ return self
304
+
305
+ def __exit__(self, exc_type, exc_val, exc_tb):
306
+ # 退出 with 块时不需要额外操作
307
+ # 因为 self.part 已经在 __lshift__ 中实时更新了
308
+ pass
309
+
310
+ def __lshift__(self, other):
311
+ """
312
+ 重载 '<<' 操作符,用于向组内添加物体。
313
+ 执行逻辑:Union (并集)
314
+ """
315
+ if isinstance(other, Entity):
316
+ if self.part is None:
317
+ # 这是一个新组,第一个物体直接作为基础
318
+ self.part = other.part
319
+ else:
320
+ # 组内已有物体,将新物体与现有物体融合 (Fuse/Union)
321
+ self.part = self.part + other.part
322
+ else:
323
+ raise ValueError("❌ Group implies adding Entity objects (Box, Cylinder, etc.)")
324
+
325
+ return self # 允许链式调用: g << part1 << part2
@@ -0,0 +1,13 @@
1
+ from igniscad import *
2
+
3
+ if __name__ == "__main__":
4
+ with Item("Robot") as item:
5
+ with Group("Leg") as leg:
6
+ foot = Box(20, 30, 5)
7
+ pole = Cylinder(5, 40).on_top_of(foot)
8
+
9
+ leg << pole << foot
10
+
11
+ item << leg.move(x=-15) << leg.move(x=15)
12
+
13
+ show()
File without changes