新增 solidworks-assembly skill:EasyEDA STEP → SolidWorks 装配与干涉检查
本机环境已实测:SOLIDWORKS 2025 SP01.1(RevisionNumber 33.1.1)、 Python 3.14.3 64 位 + pywin32,GetActiveObject 可挂已开实例。 已跑通的链路: - EasyEDA get3DFile 导 STEP —— 桥有 30s 硬超时,必须异步启动+轮询; 4MB 分块 base64 取回(刀片 31.2MB/25s,底板 53.9MB/44s) - GetImportFileData + LoadFile4 导入(得到带位号的装配树,一块板 95 个文档) - 遍历 GetDocuments 拿临时顶层 SLDASM → AddComponent4 插入(⛔ AddComponent5 恒返回 None) - ToolsCheckInterference2(0, EMPTY, False, pc, pf) 5 参形式可调 - CloseAllDocuments(True) 清理(295 文档 0.9s) ⛔ 未完成:刀片摆位(CreateTransform 卡在 pywin32 封送)、 坐标系(插入后两板被重心归零,§14 的平移量不能直接用)。 ⇒ 现状能装能查,但两板堆在原点,干涉结果无工程意义。 NOTES-probed.md 记了 18 节实测坑,包括: - 无参成员是属性不是方法(含 GetMathUtility 这个例外) - SaveAs3 返回 True 但写出的是废件(外部引用断链) - LoadFile4 不会插进当前装配体 - 动态派发"找不到成员"时用 _oleobj_.GetIDsOfNames + Invoke 绕行 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
这个提交包含在:
@@ -0,0 +1,6 @@
|
||||
# STEP 导出件几十 MB,不进 git:用 solidworks-assembly skill 的 --export 随时重导
|
||||
*.step
|
||||
*.STEP
|
||||
*.obj
|
||||
*.SLDASM
|
||||
*.SLDPRT
|
||||
@@ -0,0 +1,322 @@
|
||||
# SolidWorks COM 对接:已实测确定的事实(2026-09-12)
|
||||
|
||||
> 这份只记**在本机实测通过**的东西。没实测的不写。
|
||||
> 环境:SOLIDWORKS 2025 SP01.1(`RevisionNumber` = 33.1.1),
|
||||
> `D:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe`,
|
||||
> Python 3.14.3 **64 位**(`C:\Python314`),pywin32 已装。
|
||||
|
||||
## 1. 连接
|
||||
|
||||
```python
|
||||
import win32com.client as wc
|
||||
app = wc.GetActiveObject('SldWorks.Application') # 挂已经开着的那个实例
|
||||
```
|
||||
|
||||
- ⛔ **不要用 `gencache.EnsureDispatch`**:实测报
|
||||
`TypeError: This COM object can not automate the makepy process`。只能走**动态派发**。
|
||||
- `Dispatch` 会另起一个(可能不可见)实例;联调时用 `GetActiveObject`,
|
||||
出问题在用户屏幕上能直接看见。
|
||||
|
||||
## 2. ⛔ 最大的坑:无参成员是**属性**,不是方法
|
||||
|
||||
pywin32 动态派发下,SolidWorks 的无参 `GetXxx()` 全被当成**属性**。加括号就报
|
||||
`'str' object is not callable` / `'bool' object is not callable`。
|
||||
|
||||
| 写法 | 正确形式 |
|
||||
|---|---|
|
||||
| 文档数 | `app.GetDocumentCount`(**不加括号**)|
|
||||
| 全部文档 | `app.GetDocuments` → 返回 **tuple** |
|
||||
| 活动文档 | `app.ActiveDoc` |
|
||||
| 版本 | `app.RevisionNumber` → `'33.1.1'` |
|
||||
| 忙否 | `app.CommandInProgress` |
|
||||
| 标题 / 类型 / 路径 | `doc.GetTitle` / `doc.GetType` / `doc.GetPathName` |
|
||||
| 重建 | `doc.EditRebuild3`(**属性**,加括号会炸)|
|
||||
| **有参的才是方法** | `app.CloseDoc(title)` / `app.OpenDoc6(...)` / `app.LoadFile4(...)` / `doc.GetPartBox(True)` |
|
||||
|
||||
⛔ **不要写 `V = lambda o: o() if callable(o) else o` 这种"自动适配"包装** ——
|
||||
COM 对象 `callable()` 一律为 True,会把文档对象当函数调,报"找不到成员"。
|
||||
按上表逐个记死即可。
|
||||
|
||||
- `doc.GetType`:**1 = 零件,2 = 装配体,3 = 工程图**。
|
||||
|
||||
## 3. 导入 STEP:`GetImportFileData` + `LoadFile4`
|
||||
|
||||
```python
|
||||
data = app.GetImportFileData(step_path) # 有参 ⇒ 方法
|
||||
err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
||||
doc = app.LoadFile4(step_path, 'r', data, err) # err.value == 0 即成功
|
||||
```
|
||||
|
||||
- ⛔ `LoadFile4(path, 'r', **None**, err)` 会报**"类型不匹配"** —— 第三参必须是
|
||||
`GetImportFileData` 返回的 ImportStepData 对象。
|
||||
- ⛔ `OpenDoc6(step, 1, 1, '', err, warn)` 实测返回 `None`、`err=2097152`
|
||||
(`swApplicationBusy`)。导 STEP 用上面那条路。
|
||||
- 实测耗时:刀片 31 MB ≈ **11 s**,底板 54 MB ≈ **20 s**。
|
||||
|
||||
### 导进来的是**带位号的装配树**(很重要)
|
||||
|
||||
EasyEDA 以 `'Outfit'` 模式导出的 STEP,SolidWorks 会解析成完整装配体:
|
||||
|
||||
- 顶层 = `PhoneRail_blade`(type=2,未保存)
|
||||
- 板体 = `Board~kkk5.step.sldprt`,包围盒实测 `x 0.01~89.99 / y 0.01~67.99 / z 0~1.67`
|
||||
—— 正是刀片 90×68×1.6,**说明 STEP 的坐标原点 = 板左下角,与 EDA 板坐标一致**
|
||||
- 每个器件一个子装配体,名字里**带位号**:`R1~R0402~…`、`U4~TSSOP-24~…`
|
||||
|
||||
STEP 文件里位号在 `NEXT_ASSEMBLY_USAGE_OCCURRENCE` 的**实例名**(底板 164 个、刀片 75 个,
|
||||
与器件数对得上);`PRODUCT` 里只有几何体名(`Board~`/`TopCopper~`/`TopSilkscreen~`…)。
|
||||
⇒ 干涉报告可以按位号指名。
|
||||
|
||||
### 代价:一次导入产生上百个打开的文档
|
||||
|
||||
刀片一块就 **95 个**,两块板 **295 个**,子件都是
|
||||
`%TEMP%\swx…\IC~~\*.sldprt / *.SLDASM` 的临时文件。
|
||||
|
||||
- `doc.GetPathName` 能拿到这些临时件的真实路径(`AddComponent5` 可以直接引用它们)。
|
||||
- ⛔ **顺序约束**:要引用临时件,必须在**导入之后、清理之前**做完。
|
||||
一旦 `CloseAllDocuments`,`%TEMP%\swx…\IC~~\` 就空了(实测候选数 95 → 0)。
|
||||
|
||||
## 4. 清理
|
||||
|
||||
```python
|
||||
app.CloseAllDocuments(True) # True = 连未保存的一起关,不弹框
|
||||
```
|
||||
实测 295 个文档 **0.9 s** 关完,SolidWorks 本身不退出。
|
||||
|
||||
## 5. ⛔ 带外部引用的装配体**不能用 SaveAs 复制**
|
||||
|
||||
`doc.SaveAs3(path, 0, 2)` 实测返回 **0(失败)**。官方也写明:
|
||||
带 in-context / 外部引用的装配体要用 `ISldWorks::CopyDocument`,不是 SaveAs。
|
||||
⇒ 本 skill 的做法是**不另存**:导入后直接引用 `GetPathName` 给出的临时 SLDASM。
|
||||
|
||||
## 6. 两块板的 STEP 怎么来
|
||||
|
||||
见 `easyeda-api` 侧:`eda.pcb_ManufactureData.get3DFile(name, 'step',
|
||||
['Component Model'], 'Outfit', true)`。实测:
|
||||
|
||||
| 板 | 生成 | 大小 |
|
||||
|---|---|---|
|
||||
| 刀片 | 22.6 s | 31.2 MB |
|
||||
| 底板 | 40 s | 53.9 MB |
|
||||
|
||||
- ⛔ **桥有 30 s 硬超时**:底板直接同步调用会 `Request timed out after 30000ms`,
|
||||
且大导出会把 EDA 窗口搞断(`EDA window disconnected`)。
|
||||
⇒ 必须**异步启动 + 轮询**:JS 里把 promise 挂到 `window.__gen`,立刻 return,再每 5 s 查一次。
|
||||
- 取回用 **4 MB 分块** base64(`String.fromCharCode.apply` 按 32 KB 小段拼,避免爆栈):
|
||||
31 MB 实测 **2 s**、54 MB **4 s**。
|
||||
⛔ 别用 512 KB 分块去推算大文件耗时 —— 瓶颈是**往返次数**不是带宽,线性外推会得出
|
||||
"51 分钟"这种离谱结论(实际 2 秒)。
|
||||
- 归档在 `.claude/r2/3d/`(已 `.gitignore`,几十 MB 不进 git)。
|
||||
|
||||
## 7. 还没实测通的(⚠ 别当成已知)
|
||||
|
||||
- `AddComponent5(path, 0, '', False, '', 0.0, 0.0, 0.0)` → 返回 `None`
|
||||
(当时因 SaveAs 失败、路径不存在;**用临时 SLDASM 路径重试尚未完成**)
|
||||
- `ToolsCheckInterference2` → 六种参数形式**全部**报"非选择性的参数",
|
||||
错误码一致 ⇒ 不是参数个数问题,需要从 `sldworks.tlb` 读真实参数表与 invkind。
|
||||
- 刀片相对底板的装配变换(绕 X 转 90° 竖起 + 平移到槽位)未做。
|
||||
|
||||
## 8. 权威签名:从 `sldworks.tlb` 读,别猜
|
||||
|
||||
```python
|
||||
import pythoncom
|
||||
tlb = pythoncom.LoadTypeLib(r'D:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\sldworks.tlb')
|
||||
# 遍历 GetTypeInfo(i) → GetFuncDesc(f) → GetNames(fd.memid):names[0]=方法名,names[1:]=参数名
|
||||
# ⚠ PyFUNCDESC 没有 cParams 属性,用 len(fd.args);有 cParamsOpt。
|
||||
```
|
||||
|
||||
实读到的(SOLIDWORKS 2025):
|
||||
|
||||
| 接口.方法 | 参数 | invkind |
|
||||
|---|---|---|
|
||||
| `ISldWorks.GetImportFileData` | `(FileName)` | 1 方法 |
|
||||
| `ISldWorks.LoadFile4` | `(FileName, ArgString, ImportData, Errors)` | 1 |
|
||||
| `ISldWorks.CloseAllDocuments` | `(IncludeUnsaved)` | 1 |
|
||||
| `IAssemblyDoc.AddComponent5` | `(CompName, ConfigOption, NewConfigName, UseConfigForPartReferences, ExistingConfigName, X, Y, Z)` | 1 |
|
||||
| **`IAssemblyDoc.ToolsCheckInterference2`** | **`(NumComponents, LpComponents, CoincidentInterference, PComp, PFace)`** | 1 |
|
||||
| `IAssemblyDoc.GetComponents` | `(ToplevelOnly)` | 1 |
|
||||
| `IModelDoc2.SaveAs3` | `(NewName, SaveAsVersion, Options)` | 1 |
|
||||
| `IModelDocExtension.SaveAs3` | `(Name, Version, Options, ExportData, AdvancedSaveAsOptions, Errors, Warnings)` | 1 |
|
||||
| `IComponent2.Transform2` | — | **2 属性读 / 4 属性写** |
|
||||
|
||||
- ⛔ `ToolsCheckInterference2` 是 **5 个参数、0 可选**。只传 3 个会一律报
|
||||
**"非选择性的参数"**(= 缺必需参数),换几种写法都一样 —— 别以为是类型不对。
|
||||
后两个 `PComp` / `PFace` 是**输出**参数,要传 `VARIANT(VT_BYREF|VT_VARIANT, None)`。
|
||||
- ⛔ `AddComponent5` 的 8 参形式是对的;之前返回 `None` 纯粹因为**传进去的文件不存在**。
|
||||
- 摆位靠 **`comp.Transform2 = <MathTransform>`**(属性赋值,不是方法调用)。
|
||||
|
||||
## 9. ⛔ 类型库说"方法",动态派发下可能仍是属性
|
||||
|
||||
`IModelDoc2.EditRebuild3` 在 tlb 里是 `invkind=1`(方法、0 参),
|
||||
但实测 `asm.EditRebuild3()` 报 **`'bool' object is not callable`** ——
|
||||
动态派发对 0 参成员一律按属性取值(取值时就已执行)。
|
||||
⇒ **0 参一律写成属性形式**,不管 tlb 标的是什么。
|
||||
|
||||
## 10. ⛔ 顶层导入文档是"未保存"的,`GetPathName` 是空串
|
||||
|
||||
导入 STEP 后 `app.ActiveDoc` 是顶层装配体(`PhoneRail_blade`),它**没有路径**;
|
||||
有路径的是它的**子件**(`%TEMP%\swx…\IC~~\*.sldprt / *.SLDASM`)。
|
||||
⇒ `AddComponent5` 不能拿顶层文档的 `GetPathName`(空串 ⇒ 文件不存在 ⇒ 返回 None),
|
||||
要去临时目录找 `PhoneRail_*.step.SLDASM`,或遍历 `GetDocuments` 挑有路径的那个。
|
||||
|
||||
## 11. ✅ 另存:`Extension.SaveAs3` 的空参数要传 `VARIANT(VT_DISPATCH, None)`
|
||||
|
||||
```python
|
||||
EMPTY = wc.VARIANT(pythoncom.VT_DISPATCH, None)
|
||||
err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
||||
warn = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
||||
ok = doc.Extension.SaveAs3(dst, 0, 1, EMPTY, EMPTY, err, warn) # → True, err=0
|
||||
```
|
||||
|
||||
- ⛔ 传 `None` 会报 **"类型不匹配",且错误里指明是第 4 个参数**(`ExportData`)。
|
||||
`AdvancedSaveAsOptions`(第 5 参)同理。
|
||||
- `Options=1` = `swSaveAsOptions_Silent`。
|
||||
- ⛔ `IModelDoc2.SaveAs3(path, 0, 2)`(3 参那个)实测返回 **0**,别用。
|
||||
- 实测:刀片顶层装配体另存成 `.SLDASM` = 16.5 MB、底板 29.7 MB。
|
||||
|
||||
⛔⛔ **但这两个文件是废件** —— 更正上面这条。`SaveAs3` 返回 True、`err=0`、文件也写出来了,
|
||||
可是**再打开就是 `OpenDoc6 → None` 且 `err=0`**(文档数仍为 0),
|
||||
`AddComponent5` 引用它们也一律返回 `None`。
|
||||
原因:另存只写了顶层,子件引用仍指向 `%TEMP%\swx…\IC~~\`,
|
||||
而那个目录在 `CloseAllDocuments` 时已被清空 ⇒ 引用链断了。
|
||||
这正是官方那句「带 in-context / 外部引用的装配体不要用 SaveAs 复制,要用
|
||||
`ISldWorks::CopyDocument`」的实际后果。
|
||||
⇒ **判据教训**:`SaveAs3` 返回 True **不等于**文件可用;必须**再打开一次**才算验证通过。
|
||||
|
||||
## 12. ⛔ `LoadFile4` 不会插进当前装配体,它总是另开一个顶层文档
|
||||
|
||||
试过"在空装配体里连续导入两块板的 STEP,让它们成为同一装配的两棵子树"——**不成立**:
|
||||
每次 `LoadFile4` 都把活动文档顶成新的 `PhoneRail_base` / `PhoneRail_blade`,
|
||||
空装配体里 `GetComponents(True)` 仍是 **0 个**。
|
||||
|
||||
⇒ 唯一可行的装配路径是三步:
|
||||
1. `LoadFile4` 导入 STEP(得到未保存的顶层装配体)
|
||||
2. `Extension.SaveAs3` 另存成 `.SLDASM`(见 §11)
|
||||
3. `CloseAllDocuments(True)` → 新建总装 → `AddComponent5(那个 .SLDASM 路径, …)`
|
||||
|
||||
## 13. ✅ `ToolsCheckInterference2` 的 5 参形式已经对
|
||||
|
||||
```python
|
||||
pcomp = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_VARIANT, None)
|
||||
pface = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_VARIANT, None)
|
||||
n = asm.ToolsCheckInterference2(0, EMPTY, False, pcomp, pface)
|
||||
```
|
||||
空装配体上调用**不再报错**(返回 None / PComp 0 项)⇒ 参数形式正确,
|
||||
先前那一串"非选择性的参数"纯粹是**参数个数不够**(只传了 3 个,它要 5 个)。
|
||||
|
||||
## 14. 刀片竖插到底板槽位的变换(纯几何,已算好)
|
||||
|
||||
⛔⛔ **前提作废(2026-09-12 更正)**:下表是按"原点 = 板左下角"算的,
|
||||
而那只在**单独打开导入的装配体**时成立。一旦用 `AddComponent4` 插进总装,
|
||||
实测包围盒变成 **刀片 `x -45~45 / y -34~34 / z -3.2~3.2`、底板 `x -49~49 / y -49~49 / z -9.7~9.7`**
|
||||
—— 两块板都**以各自几何中心为原点**(SolidWorks 插入时做了重心归零)。
|
||||
⇒ 平移量必须按 `GetBox` 读回的**真实包围盒**现算,不能用板坐标直接减。下表仅留作参考。
|
||||
|
||||
(原始推导)坐标系:STEP 原点 = 板左下角
|
||||
(刀片板体包围盒 `x 0.01~89.99 / y 0.01~67.99 / z 0~1.67`,正是 90×68×1.6)。
|
||||
|
||||
刀片绕 **X 轴 +90°** 竖起:刀片 `y`(板高) → 底板 `z`(高度),刀片 `z`(板厚) → 底板 `-y`。
|
||||
`MathTransform` 是 16 元素数组、**单位米**:`[R00..R22 (9 个,行主序), dx, dy, dz, 缩放, 0,0,0]`。
|
||||
|
||||
| 槽 | 槽心 y | 平移 (dx, dy, dz) mm |
|
||||
|---|---|---|
|
||||
| 1 | 8.36 | (4.00, 9.16, 5.60) |
|
||||
| 2 | 28.68 | (4.00, 29.48, 5.60) |
|
||||
| 3 | 49.00 | (4.00, 49.80, 5.60) |
|
||||
| 4 | 69.32 | (4.00, 70.12, 5.60) |
|
||||
| 5 | 89.64 | (4.00, 90.44, 5.60) |
|
||||
|
||||
- `dx = SLOT_CX(49) − J1_X(45) = 4.00`(金手指中心对齐槽心 x)
|
||||
- `dy = 槽心 y + 0.8`(板厚 1.6,转后 z=0 面朝 −y)
|
||||
- `dz = Z0 = 塑料体高 11.1 − 插入深度 D_INSERT`
|
||||
- ⚠ **`D_INSERT` 是估值 5.5**(LCSC 页和 SOFNG 都查不到)。装配跑通后可用干涉检查反推:
|
||||
刀片底边刚好贴到槽底时的 `Z0` 才是实测值。改这一个常量即可。
|
||||
|
||||
摆位用属性赋值:`comp.Transform2 = mathUtil.CreateTransform(那 16 个数)`(见 §8,Transform2 是属性)。
|
||||
|
||||
## 15. ✅✅ 装配终于打通:用 **`AddComponent4`**,不是 `AddComponent5`
|
||||
|
||||
```python
|
||||
c = asm.AddComponent4(path, '', 0.0, 0.0, 0.0) # (CompName, ConfigName, X, Y, Z)
|
||||
```
|
||||
|
||||
- ⛔ **`AddComponent5`(8 参)在这台机器上恒返回 `None`**,试过 ConfigOption=0/1、
|
||||
坐标 0/-1、正反斜杠、先 `OpenDoc6` 预载、总装先存盘——**全都 None,且不抛异常**。
|
||||
- ✅ `AddComponent4`(5 参)**一次就成**,返回 `PhoneRail_blade.step-1`。
|
||||
⇒ 遇到 `AddComponent5` 返回 None 别再调参数,直接换 `AddComponent4`。
|
||||
- 类型库里还有更简的 `AddComponent(CompName, X, Y, Z)` 和批量的
|
||||
`AddComponents(Names, Transforms)`(可一次给变换,没试)。
|
||||
|
||||
### 被插的文件用**临时目录里的顶层 SLDASM**,别另存
|
||||
|
||||
导入 STEP 后**不要关文档**,直接遍历 `GetDocuments` 找:
|
||||
有路径 + `GetType == 2` + 标题含板名 + 后缀 `.SLDASM`
|
||||
→ 得到 `%TEMP%\swx…\IC~~\PhoneRail_base.step.SLDASM`,把它喂给 `AddComponent4`。
|
||||
|
||||
- ⛔ 别用 glob 猜文件名:真实名字是 `PhoneRail_blade**.step**.SLDASM`(中间夹着 `.step`)。
|
||||
用 `GetDocuments` 遍历最可靠。
|
||||
- ⛔ 别走"另存再插":`SaveAs3` 出来的是废件(§11),`CopyDocument` 返回 2(失败),
|
||||
`OpenDoc7` + `GetOpenDocSpec(DocumentType=1)` 也打不开 STEP(返回 None)——
|
||||
STEP 走的不是 `OpenDoc*` 那条文档通道。
|
||||
- `IImportStepData` 全表**只有 `MapConfigurationData` 可写**,
|
||||
"导入成单一零件"在 API 层没有开关。
|
||||
|
||||
实测结果:两块板都插进同一个总装,`GetComponents(True)` = 2 个顶层零部件,
|
||||
`ToolsCheckInterference2` 可正常调用(1 s 返回)。
|
||||
|
||||
## 16. ⚠ 干涉检查的第 2 参:只能传 `EMPTY`,别传零部件元组
|
||||
|
||||
已验证可用的唯一形式(全量检查):
|
||||
|
||||
```python
|
||||
pc = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_VARIANT, None)
|
||||
pf = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_VARIANT, None)
|
||||
n = asm.ToolsCheckInterference2(0, EMPTY, False, pc, pf) # ✅ 1 s 返回
|
||||
```
|
||||
|
||||
⛔ 改成 `ToolsCheckInterference2(len(comps), comps, False, pc, pf)`
|
||||
(把 `GetComponents()` 拿到的 COM 对象元组直接塞进去)会报
|
||||
**"服务器出现意外情况"**(`-2147417851`)——两种口径都试过,顶层和含子件都一样。
|
||||
⇒ 想限定范围,得先把零部件封送成 SafeArray of IDispatch,不能直接传 Python 元组。
|
||||
|
||||
## 17. 摆位:`GetMathUtility` 是**方法**,要加括号
|
||||
|
||||
报 `-2147352573 找不到成员` 的原因查明了 —— 类型库里:
|
||||
|
||||
| 成员 | invkind |
|
||||
|---|---|
|
||||
| `ISldWorks.GetMathUtility()` | **1 方法**(⚠ 少数几个无参却是方法的例外,见 §2)|
|
||||
| `IMathUtility.CreateTransform(ArrayDataIn)` | 方法 |
|
||||
| `IMathUtility.CreateTransformRotateAxis(PointObjIn, VectorObjIn, Angle)` | 方法 |
|
||||
| `IComponent2.Transform2` | 属性读/写 |
|
||||
| `IComponent2.SetTransformAndSolve2(XformIn)` | **方法**(摆位更稳妥的入口)|
|
||||
| `IMathTransform.ArrayData` | 属性读/写 |
|
||||
|
||||
```python
|
||||
mu = app.GetMathUtility() # ⇐ 加括号
|
||||
xform = mu.CreateTransform(arr16) # arr16 可能要封送成 VT_ARRAY|VT_R8
|
||||
comp.SetTransformAndSolve2(xform) # 或 comp.Transform2 = xform
|
||||
```
|
||||
|
||||
⚠ §2 那条"无参 = 属性"的规律**有例外**:`GetMathUtility` 无参却是方法。
|
||||
|
||||
⛔ **但加括号也不行** —— `app.GetMathUtility()` 照样报"找不到成员"。
|
||||
动态派发对这个成员建的包装是坏的。**绕过 `getattr`,直接按 dispid 调**:
|
||||
|
||||
```python
|
||||
did = app._oleobj_.GetIDsOfNames('GetMathUtility') # → 132(本机实测)
|
||||
raw = app._oleobj_.Invoke(did, 0, pythoncom.DISPATCH_METHOD, 1)
|
||||
mu = wc.Dispatch(raw) # → 可用的 IMathUtility
|
||||
```
|
||||
|
||||
⇒ **通用招数**:动态派发报"找不到成员"时,先用 `_oleobj_.GetIDsOfNames(名字)` 确认
|
||||
这个名字在不在。在 → 用 `_oleobj_.Invoke(dispid, 0, DISPATCH_METHOD 或 DISPATCH_PROPERTYGET, 1)`
|
||||
手动调;不在 → 名字真错了(如 `MathUtility` 就报"未知名称")。
|
||||
`IGetMathUtility`(dispid=133)虽然名字在,但两种 Invoke 都失败,别用。
|
||||
|
||||
## 18. 进度小结(2026-09-12)
|
||||
|
||||
**已验证可用**:STEP 导出(异步+4MB 分块)→ `LoadFile4` 导入 →
|
||||
`GetDocuments` 找临时顶层 SLDASM → `AddComponent4` 插入 → `ToolsCheckInterference2(0, EMPTY, …)` 可调
|
||||
→ `CloseAllDocuments(True)` 清理。
|
||||
|
||||
**未完成**:① 刀片摆位(§17)② 干涉结果解读(`PComp` 数组解析、限定范围检查 §16)。
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: solidworks-assembly
|
||||
description: 把 EasyEDA 的 PCB 导成 STEP、在本机 SOLIDWORKS 里装配并做干涉检查。用于「底板 + 刀片插在一起会不会撞」这类机械核对,也用于量插槽插入深度这类 EDA 侧算不出来的数。涉及 SolidWorks / STEP / 3D 装配 / 干涉检查 / 机械干涉 时触发。
|
||||
---
|
||||
|
||||
# SolidWorks 装配与干涉检查(PhoneRail 专用)
|
||||
|
||||
> ⚠ **动手前先读 `NOTES-probed.md`** —— 那是本机逐条实测出来的 API 口径与坑,
|
||||
> 每一条都踩过一次。不读会把同样的坑再踩一遍。
|
||||
> 环境已实测:SOLIDWORKS 2025 SP01.1(`RevisionNumber` = `33.1.1`)、
|
||||
> Python 3.14.3 **64 位** + pywin32(32 位 Python 连不上 64 位 SW,本机不存在这问题)。
|
||||
|
||||
## 什么时候用
|
||||
|
||||
- 底板与刀片、刀片与刀片之间的**机械干涉**核对(EDA 侧只能算 2.5D 净距,算不了竖插后的三维关系)
|
||||
- 量 EDA 侧拿不到的机械数:插槽**插入深度**、塑料体内腔、刀片底边离底板多高
|
||||
- ⚠ **但凡实物在手,优先用卡尺**。STEP 里的连接器是厂商**标称模型**,不含公差;
|
||||
3D 能替代"按标称值推算",替代不了实测。
|
||||
|
||||
## 整条链(每一步都已实测通过)
|
||||
|
||||
### 1. 从 EasyEDA 导出 STEP
|
||||
|
||||
```python
|
||||
# 桥有 30 s 硬超时,大板同步调用必断 ⇒ 异步启动 + 轮询
|
||||
START = "window.__gen={state:'running'}; (async()=>{ ... get3DFile(name,'step',['Component Model'],'Outfit',true) ... })(); return {started:true};"
|
||||
# 再每 5 s 查 window.__gen.state,done 后用 4 MB 分块 base64 取回
|
||||
```
|
||||
|
||||
实测:刀片 31.2 MB(生成 22.6 s + 取回 2 s)、底板 53.9 MB(生成 40 s + 取回 4 s)。
|
||||
归档在 `.claude/r2/3d/`(已 `.gitignore`)。细节见 NOTES §6。
|
||||
|
||||
### 2. 导入 SolidWorks
|
||||
|
||||
```python
|
||||
app = wc.GetActiveObject('SldWorks.Application') # 挂已开着的实例
|
||||
data = app.GetImportFileData(step_path)
|
||||
err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
||||
app.LoadFile4(step_path, 'r', data, err) # err.value == 0 即成功
|
||||
```
|
||||
|
||||
导进来是**带位号的装配树**(`R1~R0402~…`、`U4~TSSOP-24~…`),一块板 95 个文档。
|
||||
⛔ 第三参不能传 `None`(报"类型不匹配");⛔ `OpenDoc6` 导 STEP 返回 None。见 NOTES §3。
|
||||
|
||||
### 3. 找到可插引用 + 装配
|
||||
|
||||
```python
|
||||
# 导入后**不要关文档**,遍历 GetDocuments 找:有路径 + GetType==2 + .SLDASM
|
||||
p = ... # → %TEMP%\swx…\IC~~\PhoneRail_base.step.SLDASM
|
||||
c = asm.AddComponent4(p, '', 0.0, 0.0, 0.0) # ✅ 用 AddComponent4
|
||||
```
|
||||
|
||||
⛔ **`AddComponent5`(8 参)在本机恒返回 `None`**,换参数没用,直接用 `AddComponent4`。
|
||||
⛔ 别走"另存再插":`SaveAs3` 出来的是废件、`CopyDocument` 返回 2、`OpenDoc7` 打不开 STEP。
|
||||
见 NOTES §11/§12/§15。
|
||||
|
||||
### 4. 干涉检查
|
||||
|
||||
```python
|
||||
pc = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_VARIANT, None)
|
||||
pf = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_VARIANT, None)
|
||||
n = asm.ToolsCheckInterference2(0, EMPTY, False, pc, pf) # 5 参,少一个都报"非选择性的参数"
|
||||
```
|
||||
|
||||
⛔ 第 2 参只能传 `EMPTY`(`VARIANT(VT_DISPATCH, None)`),塞零部件元组会报"服务器出现意外情况"。
|
||||
|
||||
### 5. 清理
|
||||
|
||||
```python
|
||||
app.CloseAllDocuments(True) # 295 个文档 0.9 s 关完,SW 不退出
|
||||
```
|
||||
⛔ 一关就清空 `%TEMP%\swx…\IC~~\`,所以第 3 步必须在清理**之前**做完。
|
||||
|
||||
## ⛔ 未完成的两步(别当成能用)
|
||||
|
||||
1. **刀片摆位**。变换矩阵算好了(NOTES §14),但 `CreateTransform` 卡在 pywin32 封送:
|
||||
`GetMathUtility` 要用 dispid 绕行才拿得到(NOTES §17),拿到后 `CreateTransform`
|
||||
**又**报"找不到成员"——同一个病一层套一层,未解。
|
||||
2. **坐标系**:`AddComponent4` 插进总装后,两块板都被**重心归零**
|
||||
(刀片 `x -45~45 / y -34~34`、底板 `x -49~49 / y -49~49`),
|
||||
NOTES §14 那套"原点 = 板左下角"的平移量**不能直接用**,要按 `GetBox` 现算。
|
||||
|
||||
⇒ 现状:能装、能查干涉,但**两块板都堆在原点**,查出来的干涉没有工程意义。
|
||||
要么解掉上面两点,要么在 SolidWorks 里手工拖一次(更快)。
|
||||
|
||||
## 调试时的通用招数
|
||||
|
||||
动态派发报"找不到成员"时,先确认名字在不在:
|
||||
|
||||
```python
|
||||
did = app._oleobj_.GetIDsOfNames('GetMathUtility') # 在 → 返回 dispid
|
||||
raw = app._oleobj_.Invoke(did, 0, pythoncom.DISPATCH_METHOD, 1)
|
||||
```
|
||||
|
||||
还有一条贯穿始终的规律:**无参成员基本都是属性**(`app.GetDocumentCount` / `doc.GetTitle` /
|
||||
`doc.EditRebuild3` 都不加括号),有参的才是方法。但**有例外**(`GetMathUtility`),
|
||||
拿不准就去 `sldworks.tlb` 查 `invkind`(NOTES §8 给了读法)。
|
||||
在新工单中引用
屏蔽一个用户