swforge 0.9.1__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.
- swforge/__init__.py +60 -0
- swforge/assembly.py +322 -0
- swforge/batch.py +263 -0
- swforge/cleanup.py +214 -0
- swforge/cli.py +363 -0
- swforge/colors.py +280 -0
- swforge/dims.py +336 -0
- swforge/drawing.py +897 -0
- swforge/gb_materials.py +268 -0
- swforge/interference.py +222 -0
- swforge/mates.py +452 -0
- swforge/modeling.py +829 -0
- swforge/probe.py +284 -0
- swforge/resources.py +312 -0
- swforge/server.py +410 -0
- swforge/session.py +377 -0
- swforge/spec.py +92 -0
- swforge-0.9.1.dist-info/METADATA +402 -0
- swforge-0.9.1.dist-info/RECORD +23 -0
- swforge-0.9.1.dist-info/WHEEL +5 -0
- swforge-0.9.1.dist-info/entry_points.txt +3 -0
- swforge-0.9.1.dist-info/licenses/LICENSE +21 -0
- swforge-0.9.1.dist-info/top_level.txt +1 -0
swforge/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""swforge —— SolidWorks 自动化工具箱。
|
|
3
|
+
|
|
4
|
+
把两类能力合到一起:
|
|
5
|
+
|
|
6
|
+
* **几何**:声明式 DSL → 生成 C# 早期绑定 → 编译执行(批量建模快 3~10 倍)
|
|
7
|
+
* **出图**:委托 AssetMate_MechDog 做标注与自检,本地负责排版与成册
|
|
8
|
+
|
|
9
|
+
快速开始
|
|
10
|
+
--------
|
|
11
|
+
>>> from swforge import PartBuilder, FaceRule, apply_rules
|
|
12
|
+
>>> b = PartBuilder(r"D:\\out\\SWFORGE_支架.SLDPRT")
|
|
13
|
+
>>> b.box("底板", 0, 200, 0, 10, 0, 200, "steel")
|
|
14
|
+
>>> b.tube_y("立柱", 0, 1000, 0, 0, 150, 150, wall=6.3, color="yellow")
|
|
15
|
+
>>> r = b.build()
|
|
16
|
+
>>> r.ok, r.bodies
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .session import SWSession, get_session, SWError, BridgeLost
|
|
20
|
+
from .modeling import PartBuilder, BuildResult, Material, COLORS, preview_edges
|
|
21
|
+
from .resources import (document_inventory, resource_status, sweep_documents,
|
|
22
|
+
ensure_capacity)
|
|
23
|
+
from .spec import builder_from_spec
|
|
24
|
+
from .gb_materials import GB_MATERIALS, ensure_library, material_names
|
|
25
|
+
from .assembly import AssemblyBuilder, list_components, check_interference
|
|
26
|
+
from .interference import body_interference, check_all_pairs
|
|
27
|
+
from .mates import MateBuilder, list_mates
|
|
28
|
+
from .dims import add_overall_dimensions
|
|
29
|
+
from .batch import batch_drawings, collect_models, write_report
|
|
30
|
+
from .probe import probe_api
|
|
31
|
+
from .colors import FaceRule, apply_rules, read_back
|
|
32
|
+
from .cleanup import survey, delete_bodies, purge_giant, purge_small
|
|
33
|
+
from .drawing import (have_mechdog, produce, export_pdf, normalize_layout,
|
|
34
|
+
make_atlas, delivery_check, add_dimensions, count_dimensions)
|
|
35
|
+
|
|
36
|
+
__version__ = "0.9.1"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"__version__",
|
|
40
|
+
# session
|
|
41
|
+
"SWSession", "get_session", "SWError", "BridgeLost",
|
|
42
|
+
# modeling
|
|
43
|
+
"PartBuilder", "BuildResult", "Material", "COLORS", "builder_from_spec", "preview_edges",
|
|
44
|
+
"document_inventory", "resource_status", "sweep_documents", "ensure_capacity",
|
|
45
|
+
"GB_MATERIALS", "material_names", "ensure_library",
|
|
46
|
+
# assembly
|
|
47
|
+
"AssemblyBuilder", "list_components", "check_interference", "body_interference", "check_all_pairs",
|
|
48
|
+
# mates / dims
|
|
49
|
+
"MateBuilder", "list_mates", "add_overall_dimensions",
|
|
50
|
+
# batch
|
|
51
|
+
"batch_drawings", "collect_models", "write_report", "probe_api",
|
|
52
|
+
# colors
|
|
53
|
+
"FaceRule", "apply_rules", "read_back",
|
|
54
|
+
# cleanup
|
|
55
|
+
"survey", "delete_bodies", "purge_giant", "purge_small",
|
|
56
|
+
# drawing
|
|
57
|
+
"have_mechdog", "produce", "export_pdf", "normalize_layout",
|
|
58
|
+
"make_atlas", "delivery_check", "add_dimensions", "count_dimensions",
|
|
59
|
+
]
|
|
60
|
+
|
swforge/assembly.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""装配体:插入零件、固定基件、跑干涉检查。
|
|
3
|
+
|
|
4
|
+
关键实测结论(本机 SW2025 SP5.0)
|
|
5
|
+
--------------------------------
|
|
6
|
+
1. **零件必须先打开**再 `AddComponent5`,否则返回 None(不报错,静默失败)。
|
|
7
|
+
这是本环境最反直觉的一条:直接传文件路径给一个未打开的零件,一律失败。
|
|
8
|
+
2. `GetComponents(False)` 能拿到组件清单;组件的 `Name2` 形如 `a_base-1`。
|
|
9
|
+
3. 干涉检查走 `IAssemblyDoc.GetInterferenceDetectionManager`,
|
|
10
|
+
读完必须调用 `Done()` 释放。
|
|
11
|
+
|
|
12
|
+
配合(Mate)
|
|
13
|
+
-----------
|
|
14
|
+
`CreateMate` / `AddMate5` 需要先选中两个实体的面,本版**没有实现自动选面**,
|
|
15
|
+
只提供 `list_components()` 与 `check_interference()` 这类不依赖选择的检查。
|
|
16
|
+
配合请先在 SW 界面里建立,再用本模块做干涉验证。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
from .session import get_session, _value, SWError
|
|
24
|
+
|
|
25
|
+
__all__ = ["AssemblyBuilder", "list_components", "check_interference"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _abs(path: str) -> str:
|
|
29
|
+
if not path or not os.path.isfile(path):
|
|
30
|
+
raise ValueError(f"零件文件不存在:{path}")
|
|
31
|
+
return os.path.abspath(path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _default_asm_template() -> str:
|
|
35
|
+
cands = []
|
|
36
|
+
env = os.environ.get("SW_TEMPLATE_DIR")
|
|
37
|
+
if env:
|
|
38
|
+
cands.append(os.path.join(env, "Assembly.asmdot"))
|
|
39
|
+
base = os.environ.get("PROGRAMDATA") or r"C:\ProgramData"
|
|
40
|
+
for year in range(2026, 2019, -1):
|
|
41
|
+
for vendor in ("SOLIDWORKS", "SolidWorks"):
|
|
42
|
+
cands.append(os.path.join(base, vendor, f"SOLIDWORKS {year}",
|
|
43
|
+
"templates", "gb_assembly.asmdot"))
|
|
44
|
+
cands.append(os.path.join(base, vendor, f"SOLIDWORKS {year}",
|
|
45
|
+
"templates", "Assembly.asmdot"))
|
|
46
|
+
for p in cands:
|
|
47
|
+
if os.path.isfile(p):
|
|
48
|
+
return p
|
|
49
|
+
raise RuntimeError("找不到装配模板,请设置 SW_TEMPLATE_DIR")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _activate(sw, md):
|
|
53
|
+
"""把指定文档设为活动文档。"""
|
|
54
|
+
from win32com.client import VARIANT
|
|
55
|
+
import pythoncom
|
|
56
|
+
|
|
57
|
+
title = str(_value(md, "GetTitle"))
|
|
58
|
+
err = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
|
59
|
+
sw.ActivateDoc3(title, False, 0, err)
|
|
60
|
+
return sw.ActiveDoc
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _open_part(sw, path: str):
|
|
64
|
+
"""打开零件并返回文档对象;已打开则直接复用。"""
|
|
65
|
+
target = os.path.normcase(os.path.abspath(path))
|
|
66
|
+
for d in (_value(sw, "GetDocuments") or []):
|
|
67
|
+
try:
|
|
68
|
+
cur = str(_value(d, "GetPathName"))
|
|
69
|
+
except Exception: # noqa: BLE001
|
|
70
|
+
continue
|
|
71
|
+
if cur and os.path.normcase(os.path.abspath(cur)) == target:
|
|
72
|
+
return d
|
|
73
|
+
from win32com.client import VARIANT
|
|
74
|
+
import pythoncom
|
|
75
|
+
|
|
76
|
+
err = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
|
77
|
+
warn = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
|
78
|
+
doc = sw.OpenDoc6(path, 1, 1, "", err, warn) # 1 = swDocPART
|
|
79
|
+
if doc is None:
|
|
80
|
+
raise SWError(f"打开零件失败:{path}")
|
|
81
|
+
return doc
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _fix_component(ad, comp) -> str:
|
|
85
|
+
"""把组件固定。逐级尝试不同选择 API,返回实际生效的那条。"""
|
|
86
|
+
errors = []
|
|
87
|
+
try:
|
|
88
|
+
ad.ClearSelection2(True)
|
|
89
|
+
except Exception: # noqa: BLE001
|
|
90
|
+
pass
|
|
91
|
+
for label, sel in (
|
|
92
|
+
("Select4(True,None,False)", lambda: comp.Select4(True, None, False)),
|
|
93
|
+
("Select4(True,None)", lambda: comp.Select4(True, None)),
|
|
94
|
+
("Select(True)", lambda: comp.Select(True)),
|
|
95
|
+
("Select2(True,0)", lambda: comp.Select2(True, 0)),
|
|
96
|
+
):
|
|
97
|
+
try:
|
|
98
|
+
sel()
|
|
99
|
+
ok = ad.FixComponent()
|
|
100
|
+
return f"{label} -> FixComponent={ok}"
|
|
101
|
+
except Exception as exc: # noqa: BLE001
|
|
102
|
+
errors.append(f"{label}: {type(exc).__name__}")
|
|
103
|
+
return "失败(" + "; ".join(errors) + ")"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AssemblyBuilder:
|
|
107
|
+
"""按顺序插入零件并记录,最后一次性建出装配体。
|
|
108
|
+
|
|
109
|
+
用法::
|
|
110
|
+
|
|
111
|
+
a = AssemblyBuilder(r"D:\\out\\总装.SLDASM")
|
|
112
|
+
a.add(r"D:\\out\\底座.SLDPRT", x=0, y=0, z=0, fixed=True)
|
|
113
|
+
a.add(r"D:\\out\\立柱.SLDPRT", x=0, y=0, z=0)
|
|
114
|
+
r = a.build()
|
|
115
|
+
print(r["ok"], r["components"])
|
|
116
|
+
print(check_interference())
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, path: str, template: str | None = None,
|
|
120
|
+
keep_open: bool = True):
|
|
121
|
+
if not str(path).lower().endswith(".sldasm"):
|
|
122
|
+
raise ValueError("装配体输出必须使用 .SLDASM 扩展名")
|
|
123
|
+
self.path = os.path.abspath(path)
|
|
124
|
+
self.template = template
|
|
125
|
+
self.keep_open = keep_open
|
|
126
|
+
self._items = [] # (path, x, y, z, fixed)
|
|
127
|
+
|
|
128
|
+
def add(self, part_path: str, x: float = 0.0, y: float = 0.0, z: float = 0.0,
|
|
129
|
+
fixed: bool = False) -> "AssemblyBuilder":
|
|
130
|
+
"""插入零件。**坐标单位是毫米**(与全项目一致)。
|
|
131
|
+
|
|
132
|
+
SolidWorks 的 `AddComponent5` 内部要米,这里统一换算;
|
|
133
|
+
v0.7 及以前直接把毫米当米传,导致零件被放到 60 米外。
|
|
134
|
+
"""
|
|
135
|
+
self._items.append((_abs(part_path), float(x), float(y), float(z), bool(fixed)))
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------ 构建
|
|
139
|
+
def build(self, max_documents: int = 8) -> dict:
|
|
140
|
+
from .resources import ensure_capacity
|
|
141
|
+
|
|
142
|
+
if not self._items:
|
|
143
|
+
return {"ok": False, "error": "没有要插入的零件"}
|
|
144
|
+
cap = ensure_capacity(need=1, limit=max_documents)
|
|
145
|
+
if not cap["allowed"]:
|
|
146
|
+
return {"ok": False, "error": cap["error"], "resource": cap}
|
|
147
|
+
|
|
148
|
+
sw = get_session().sw()
|
|
149
|
+
log = []
|
|
150
|
+
asm = None
|
|
151
|
+
|
|
152
|
+
def L(msg):
|
|
153
|
+
log.append(str(msg))
|
|
154
|
+
print(msg)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
tpl = self.template or _default_asm_template()
|
|
158
|
+
L(f"模板: {tpl}")
|
|
159
|
+
asm = sw.NewDocument(tpl, 0, 0, 0)
|
|
160
|
+
if asm is None:
|
|
161
|
+
return {"ok": False, "error": "新建装配文档失败", "log": "\n".join(log)}
|
|
162
|
+
|
|
163
|
+
ad = asm # 后期绑定同一个 dispatch
|
|
164
|
+
inserted = []
|
|
165
|
+
for path, x, y, z, fixed in self._items:
|
|
166
|
+
# ① 零件必须先打开,否则 AddComponent5 静默返回 None
|
|
167
|
+
_open_part(sw, path)
|
|
168
|
+
asm = _activate(sw, asm)
|
|
169
|
+
comp = None
|
|
170
|
+
try:
|
|
171
|
+
comp = ad.AddComponent5(path, 0, "", False, "",
|
|
172
|
+
x / 1000.0, y / 1000.0, z / 1000.0)
|
|
173
|
+
except Exception as exc: # noqa: BLE001
|
|
174
|
+
L(f"AddComponent5 异常 {os.path.basename(path)}: {exc}")
|
|
175
|
+
if comp is None:
|
|
176
|
+
try:
|
|
177
|
+
comp = ad.AddComponent4(path, "", x / 1000.0, y / 1000.0, z / 1000.0)
|
|
178
|
+
except Exception: # noqa: BLE001
|
|
179
|
+
pass
|
|
180
|
+
if comp is None:
|
|
181
|
+
L(f"插入失败: {os.path.basename(path)}")
|
|
182
|
+
continue
|
|
183
|
+
name = str(_value(comp, "Name2"))
|
|
184
|
+
L(f"插入 {name} @ ({x},{y},{z})")
|
|
185
|
+
if fixed:
|
|
186
|
+
L(f"固定 {name} -> {_fix_component(ad, comp)}")
|
|
187
|
+
inserted.append({"part": path, "component": name,
|
|
188
|
+
"position": [x, y, z], "fixed": fixed})
|
|
189
|
+
|
|
190
|
+
comps = self._components(ad)
|
|
191
|
+
L(f"组件数 = {len(comps)}")
|
|
192
|
+
ok = len(inserted) == len(self._items) and len(comps) >= len(self._items)
|
|
193
|
+
saved = False
|
|
194
|
+
if ok:
|
|
195
|
+
from win32com.client import VARIANT
|
|
196
|
+
import pythoncom
|
|
197
|
+
err = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
|
198
|
+
warn = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
|
199
|
+
# ExportData 是 IDispatch 指针;直接传 None 会变成 VT_EMPTY 导致类型不匹配
|
|
200
|
+
export_data = VARIANT(pythoncom.VT_DISPATCH, None)
|
|
201
|
+
try:
|
|
202
|
+
saved = bool(asm.Extension.SaveAs(self.path, 0, 1, export_data,
|
|
203
|
+
err, warn))
|
|
204
|
+
L(f"保存 {saved} err={err.value}")
|
|
205
|
+
except Exception as exc: # noqa: BLE001
|
|
206
|
+
L(f"保存异常: {exc}")
|
|
207
|
+
return {
|
|
208
|
+
"ok": bool(ok and saved),
|
|
209
|
+
"path": self.path,
|
|
210
|
+
"components": comps,
|
|
211
|
+
"inserted": inserted,
|
|
212
|
+
"saved": saved,
|
|
213
|
+
"log": "\n".join(log),
|
|
214
|
+
}
|
|
215
|
+
finally:
|
|
216
|
+
if asm is not None and not self.keep_open:
|
|
217
|
+
try:
|
|
218
|
+
sw.CloseDoc(str(_value(asm, "GetTitle")))
|
|
219
|
+
except Exception: # noqa: BLE001
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _components(ad) -> list[dict]:
|
|
224
|
+
try:
|
|
225
|
+
raw = ad.GetComponents(False)
|
|
226
|
+
except Exception: # noqa: BLE001
|
|
227
|
+
return []
|
|
228
|
+
out = []
|
|
229
|
+
for c in (raw or []):
|
|
230
|
+
try:
|
|
231
|
+
out.append({
|
|
232
|
+
"name": str(_value(c, "Name2")),
|
|
233
|
+
"path": str(_value(c, "GetPathName")),
|
|
234
|
+
"suppressed": bool(_value(c, "IsSuppressed")),
|
|
235
|
+
})
|
|
236
|
+
except Exception: # noqa: BLE001
|
|
237
|
+
out.append({"name": str(c), "path": "", "suppressed": False})
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------- 只读接口
|
|
242
|
+
def list_components(assembly_path: str | None = None) -> dict:
|
|
243
|
+
"""列出当前(或指定)装配体的组件。"""
|
|
244
|
+
sw = get_session().sw()
|
|
245
|
+
md = sw.ActiveDoc
|
|
246
|
+
if assembly_path:
|
|
247
|
+
md = sw.OpenDoc(assembly_path, 2) # 2 = swDocASSEMBLY
|
|
248
|
+
sw.ActivateDoc(assembly_path)
|
|
249
|
+
md = sw.ActiveDoc
|
|
250
|
+
if md is None or _value(md, "GetType") != 2:
|
|
251
|
+
raise RuntimeError("当前文档不是装配体")
|
|
252
|
+
comps = AssemblyBuilder._components(md)
|
|
253
|
+
return {"ok": True, "path": str(_value(md, "GetPathName")),
|
|
254
|
+
"count": len(comps), "components": comps}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def check_interference(assembly_path: str | None = None,
|
|
258
|
+
treat_coincident_as_interference: bool = False) -> dict:
|
|
259
|
+
"""跑干涉检查并返回干涉清单。
|
|
260
|
+
|
|
261
|
+
读完后调用 `Done()` 释放管理器。
|
|
262
|
+
"""
|
|
263
|
+
sw = get_session().sw()
|
|
264
|
+
md = sw.ActiveDoc
|
|
265
|
+
if assembly_path:
|
|
266
|
+
md = sw.OpenDoc(assembly_path, 2)
|
|
267
|
+
sw.ActivateDoc(assembly_path)
|
|
268
|
+
md = sw.ActiveDoc
|
|
269
|
+
if md is None or _value(md, "GetType") != 2:
|
|
270
|
+
return {"ok": False, "error": "当前文档不是装配体"}
|
|
271
|
+
|
|
272
|
+
# 后期绑定下这是**属性**(InterferenceDetectionManager),
|
|
273
|
+
# 方法名 GetInterferenceDetectionManager 取不到。
|
|
274
|
+
mgr = None
|
|
275
|
+
last = ""
|
|
276
|
+
for name in ("InterferenceDetectionManager", "GetInterferenceDetectionManager"):
|
|
277
|
+
try:
|
|
278
|
+
got = _value(md, name)
|
|
279
|
+
got = got() if callable(got) and not hasattr(got, "GetInterferenceCount") else got
|
|
280
|
+
if got is not None:
|
|
281
|
+
mgr = got
|
|
282
|
+
break
|
|
283
|
+
except Exception as exc: # noqa: BLE001
|
|
284
|
+
last = f"{name}: {type(exc).__name__}"
|
|
285
|
+
if mgr is None:
|
|
286
|
+
return {"ok": False, "error": f"取干涉管理器失败({last})"}
|
|
287
|
+
if mgr is None:
|
|
288
|
+
return {"ok": False, "error": "干涉管理器为空"}
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
try:
|
|
292
|
+
mgr.TreatCoincidenceAsInterference = bool(treat_coincident_as_interference)
|
|
293
|
+
except Exception: # noqa: BLE001
|
|
294
|
+
pass
|
|
295
|
+
count = 0
|
|
296
|
+
items = []
|
|
297
|
+
try:
|
|
298
|
+
count = int(_value(mgr, "GetInterferenceCount") or 0)
|
|
299
|
+
except Exception: # noqa: BLE001
|
|
300
|
+
count = 0
|
|
301
|
+
if count:
|
|
302
|
+
try:
|
|
303
|
+
raw = mgr.GetInterferences()
|
|
304
|
+
except Exception: # noqa: BLE001
|
|
305
|
+
raw = None
|
|
306
|
+
for x in (raw or []):
|
|
307
|
+
try:
|
|
308
|
+
items.append({
|
|
309
|
+
"volume_mm3": round(float(_value(x, "Volume")) * 1e9, 3),
|
|
310
|
+
"components": [str(_value(c, "Name2")) for c in
|
|
311
|
+
(x.Components if hasattr(x, "Components") else [])],
|
|
312
|
+
})
|
|
313
|
+
except Exception: # noqa: BLE001
|
|
314
|
+
items.append({"raw": str(x)})
|
|
315
|
+
try:
|
|
316
|
+
mgr.Done()
|
|
317
|
+
except Exception: # noqa: BLE001
|
|
318
|
+
pass
|
|
319
|
+
return {"ok": True, "interference_count": count, "interferences": items,
|
|
320
|
+
"note": "count=0 表示无干涉;重合面是否算干涉由参数控制"}
|
|
321
|
+
except Exception as exc: # noqa: BLE001
|
|
322
|
+
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
swforge/batch.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""批量出图:一个文件夹的零件 → 一批带 GB 图框的工程图。
|
|
3
|
+
|
|
4
|
+
为什么这个功能重要
|
|
5
|
+
------------------
|
|
6
|
+
单件出图只是"省一点手",**批量出图才是质变**:
|
|
7
|
+
|
|
8
|
+
以前:50 个零件,每件 20~40 分钟出图 → 两三天
|
|
9
|
+
现在:50 个零件,一批跑完 → 十几分钟(还要人工补公差)
|
|
10
|
+
|
|
11
|
+
而且它天然适合演示 —— 屏幕上不是"出了一张图",是"刷出来一列图"。
|
|
12
|
+
|
|
13
|
+
设计要点
|
|
14
|
+
--------
|
|
15
|
+
1. **走已有的 produce() 链路**,不重造轮子,所以图框、标注、排版、PDF 全都自动带上
|
|
16
|
+
2. **逐件隔离失败**:一个零件崩了不能拖垮整批,失败原因单独记
|
|
17
|
+
3. **资源守卫**:SolidWorks 默认 8 个文档上限,所以每件出完就关
|
|
18
|
+
4. **可中断可续跑**:已经存在的图默认跳过(`--skip-existing`),断了能接着跑
|
|
19
|
+
5. **进度可见**:每件打一行,长任务最怕没反馈
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import fnmatch
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import time
|
|
28
|
+
from dataclasses import dataclass, field, asdict
|
|
29
|
+
|
|
30
|
+
__all__ = ["batch_drawings", "collect_models"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------- 模型收集
|
|
34
|
+
def collect_models(folder: str, pattern: str = "*.SLDPRT",
|
|
35
|
+
recursive: bool = False) -> list[str]:
|
|
36
|
+
"""收集待出图的模型路径。
|
|
37
|
+
|
|
38
|
+
pattern 不区分大小写(Windows 上 .SLDPRT / .sldprt 都存在)。
|
|
39
|
+
"""
|
|
40
|
+
folder = os.path.abspath(folder)
|
|
41
|
+
if not os.path.isdir(folder):
|
|
42
|
+
raise ValueError(f"不是文件夹:{folder}")
|
|
43
|
+
|
|
44
|
+
pat = pattern.lower()
|
|
45
|
+
out: list[str] = []
|
|
46
|
+
|
|
47
|
+
if recursive:
|
|
48
|
+
for dirpath, dirnames, filenames in os.walk(folder):
|
|
49
|
+
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
|
50
|
+
for fn in filenames:
|
|
51
|
+
if fnmatch.fnmatch(fn.lower(), pat):
|
|
52
|
+
out.append(os.path.join(dirpath, fn))
|
|
53
|
+
else:
|
|
54
|
+
for fn in os.listdir(folder):
|
|
55
|
+
full = os.path.join(folder, fn)
|
|
56
|
+
if os.path.isfile(full) and fnmatch.fnmatch(fn.lower(), pat):
|
|
57
|
+
out.append(full)
|
|
58
|
+
|
|
59
|
+
return sorted(out)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _release(*paths: str) -> None:
|
|
63
|
+
"""关掉指向这些路径的已打开文档,释放 SolidWorks 文档名额。"""
|
|
64
|
+
try:
|
|
65
|
+
from .drawing import _close_open_document
|
|
66
|
+
except Exception:
|
|
67
|
+
return
|
|
68
|
+
for p in paths:
|
|
69
|
+
if not p:
|
|
70
|
+
continue
|
|
71
|
+
try:
|
|
72
|
+
_close_open_document(p)
|
|
73
|
+
except Exception:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------- 结果结构
|
|
78
|
+
@dataclass
|
|
79
|
+
class ItemResult:
|
|
80
|
+
model: str
|
|
81
|
+
drawing: str = ""
|
|
82
|
+
ok: bool = False
|
|
83
|
+
skipped: bool = False
|
|
84
|
+
dims: int = 0
|
|
85
|
+
overall_done: int = 0
|
|
86
|
+
pdf: str = ""
|
|
87
|
+
pdf_ok: bool = False
|
|
88
|
+
seconds: float = 0.0
|
|
89
|
+
error: str = ""
|
|
90
|
+
|
|
91
|
+
def to_dict(self):
|
|
92
|
+
return asdict(self)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class BatchResult:
|
|
97
|
+
ok: bool = True
|
|
98
|
+
folder: str = ""
|
|
99
|
+
total: int = 0
|
|
100
|
+
succeeded: int = 0
|
|
101
|
+
failed: int = 0
|
|
102
|
+
skipped: int = 0
|
|
103
|
+
pdf_ok: int = 0
|
|
104
|
+
seconds: float = 0.0
|
|
105
|
+
items: list = field(default_factory=list)
|
|
106
|
+
error: str = ""
|
|
107
|
+
|
|
108
|
+
def to_dict(self):
|
|
109
|
+
d = asdict(self)
|
|
110
|
+
return d
|
|
111
|
+
|
|
112
|
+
def summary_line(self) -> str:
|
|
113
|
+
return (f"{self.succeeded}/{self.total} 成功"
|
|
114
|
+
f",失败 {self.failed},跳过 {self.skipped}"
|
|
115
|
+
f",PDF {self.pdf_ok},耗时 {self.seconds:.1f} 秒")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------- 主流程
|
|
119
|
+
def batch_drawings(folder: str,
|
|
120
|
+
out_dir: str | None = None,
|
|
121
|
+
pattern: str = "*.SLDPRT",
|
|
122
|
+
recursive: bool = False,
|
|
123
|
+
paper: str = "A3",
|
|
124
|
+
overall: bool = True,
|
|
125
|
+
pdf: bool = False,
|
|
126
|
+
skip_existing: bool = True,
|
|
127
|
+
limit: int = 0,
|
|
128
|
+
max_documents: int = 8,
|
|
129
|
+
progress=None) -> dict:
|
|
130
|
+
"""批量出图。
|
|
131
|
+
|
|
132
|
+
参数
|
|
133
|
+
----
|
|
134
|
+
folder 模型所在文件夹
|
|
135
|
+
out_dir 输出文件夹(默认与模型同目录)
|
|
136
|
+
pattern 文件名通配,默认 `*.SLDPRT`
|
|
137
|
+
recursive 是否递归子目录
|
|
138
|
+
paper 纸张 A0~A4
|
|
139
|
+
overall 是否补总尺寸
|
|
140
|
+
pdf 是否同时导 PDF
|
|
141
|
+
skip_existing 目标图已存在时跳过(支持断点续跑)
|
|
142
|
+
limit 最多处理几个(0 = 不限,调试用)
|
|
143
|
+
max_documents 单批允许占用的 SW 文档数
|
|
144
|
+
progress 回调 fn(idx, total, item: ItemResult),用于实时打印
|
|
145
|
+
|
|
146
|
+
返回 `BatchResult.to_dict()`。
|
|
147
|
+
|
|
148
|
+
注意
|
|
149
|
+
----
|
|
150
|
+
这个函数**不吞异常**:单件失败会记进 items 并在 summary 里体现,
|
|
151
|
+
但不会中断整批 —— 批量任务里"一个坏件毁掉全部"是不可接受的。
|
|
152
|
+
"""
|
|
153
|
+
from .drawing import produce, add_dimensions
|
|
154
|
+
from .resources import sweep_documents
|
|
155
|
+
|
|
156
|
+
started = time.monotonic()
|
|
157
|
+
folder = os.path.abspath(folder)
|
|
158
|
+
out_dir = os.path.abspath(out_dir) if out_dir else folder
|
|
159
|
+
|
|
160
|
+
models = collect_models(folder, pattern=pattern, recursive=recursive)
|
|
161
|
+
if limit and limit > 0:
|
|
162
|
+
models = models[:limit]
|
|
163
|
+
|
|
164
|
+
result = BatchResult(folder=folder, total=len(models))
|
|
165
|
+
if not models:
|
|
166
|
+
result.ok = False
|
|
167
|
+
result.error = f"在 {folder} 里没找到匹配 {pattern} 的文件"
|
|
168
|
+
result.seconds = round(time.monotonic() - started, 2)
|
|
169
|
+
return result.to_dict()
|
|
170
|
+
|
|
171
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
for idx, model in enumerate(models, 1):
|
|
174
|
+
item = ItemResult(model=model)
|
|
175
|
+
t0 = time.monotonic()
|
|
176
|
+
|
|
177
|
+
stem = os.path.splitext(os.path.basename(model))[0]
|
|
178
|
+
dwg = os.path.join(out_dir, stem + ".SLDDRW")
|
|
179
|
+
pdf_path = os.path.join(out_dir, stem + ".pdf")
|
|
180
|
+
item.drawing = dwg
|
|
181
|
+
|
|
182
|
+
# ---- 已存在就跳过
|
|
183
|
+
if skip_existing and os.path.isfile(dwg):
|
|
184
|
+
item.skipped = True
|
|
185
|
+
item.ok = True
|
|
186
|
+
item.seconds = 0.0
|
|
187
|
+
result.skipped += 1
|
|
188
|
+
result.items.append(item.to_dict())
|
|
189
|
+
if progress:
|
|
190
|
+
progress(idx, result.total, item)
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
# 每件开始前清一次场:SolidWorks 默认 8 文档上限,
|
|
195
|
+
# 批量场景下不清就会在第 8 件之后全部「静默失败」
|
|
196
|
+
try:
|
|
197
|
+
sweep_documents(dry_run=False)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
r = produce(model, dwg, paper=paper,
|
|
202
|
+
keep_open=True, max_documents=max_documents)
|
|
203
|
+
if not r.get("ok"):
|
|
204
|
+
item.error = str(r.get("error") or f"produce err={r.get('err')}")
|
|
205
|
+
item.seconds = round(time.monotonic() - t0, 2)
|
|
206
|
+
result.failed += 1
|
|
207
|
+
result.items.append(item.to_dict())
|
|
208
|
+
if progress:
|
|
209
|
+
progress(idx, result.total, item)
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# 标注(含总尺寸)
|
|
213
|
+
try:
|
|
214
|
+
d = add_dimensions(dwg, overall=overall)
|
|
215
|
+
item.dims = int(d.get("after") or 0)
|
|
216
|
+
ov = d.get("overall") or {}
|
|
217
|
+
item.overall_done = int(ov.get("done") or 0)
|
|
218
|
+
except Exception as exc: # noqa: BLE001
|
|
219
|
+
item.error = f"标注失败:{type(exc).__name__}: {exc}"
|
|
220
|
+
|
|
221
|
+
# 可选 PDF
|
|
222
|
+
if pdf:
|
|
223
|
+
try:
|
|
224
|
+
from .drawing import export_pdf
|
|
225
|
+
pr = export_pdf(dwg, pdf_path)
|
|
226
|
+
item.pdf = pdf_path
|
|
227
|
+
item.pdf_ok = bool(pr.get("ok"))
|
|
228
|
+
if item.pdf_ok:
|
|
229
|
+
result.pdf_ok += 1
|
|
230
|
+
except Exception as exc: # noqa: BLE001
|
|
231
|
+
item.error = (item.error + ";" if item.error else "") + \
|
|
232
|
+
f"PDF 失败:{type(exc).__name__}: {exc}"
|
|
233
|
+
|
|
234
|
+
item.ok = not item.error
|
|
235
|
+
if item.ok:
|
|
236
|
+
result.succeeded += 1
|
|
237
|
+
else:
|
|
238
|
+
result.failed += 1
|
|
239
|
+
|
|
240
|
+
except Exception as exc: # noqa: BLE001
|
|
241
|
+
item.error = f"{type(exc).__name__}: {exc}"
|
|
242
|
+
result.failed += 1
|
|
243
|
+
finally:
|
|
244
|
+
# 每件结束必须释放文档。SolidWorks 默认只允许 8 个文档同时打开,
|
|
245
|
+
# 批处理不清场会在第 8 件之后整批静默失败 —— 实测第 5 件就撞上了
|
|
246
|
+
# (模型 + 工程图各占一个,加上前一件残留)。
|
|
247
|
+
_release(model, dwg)
|
|
248
|
+
item.seconds = round(time.monotonic() - t0, 2)
|
|
249
|
+
result.items.append(item.to_dict())
|
|
250
|
+
if progress:
|
|
251
|
+
progress(idx, result.total, item)
|
|
252
|
+
|
|
253
|
+
result.seconds = round(time.monotonic() - started, 2)
|
|
254
|
+
result.ok = result.failed == 0 and result.succeeded > 0
|
|
255
|
+
return result.to_dict()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def write_report(result: dict, path: str) -> str:
|
|
259
|
+
"""把批量结果写成 JSON 报告,便于事后查哪件失败了。"""
|
|
260
|
+
os.makedirs(os.path.dirname(os.path.abspath(path)) or ".", exist_ok=True)
|
|
261
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
262
|
+
json.dump(result, fh, ensure_ascii=False, indent=2)
|
|
263
|
+
return path
|