用户已在 SolidWorks 里手工把两块板载入同一装配体,刀片尚未摆位。 在 PROGRESS 顶部加「新会话从这里开始」作为唯一入口:当前装配体状态、 下一步(刀片绕 X +90° 竖起 + 平移到槽位)、卡点(CreateTransform 封送)、 本会话七项成果与对应提交、四条待办。 装配体状态快照存 .claude/r2/3d/_sw_assembly_state.json: - 活动文档 D:\DCDC\新建文件夹\3D_BASE-PCB_2026-09-12.SLDASM - 底板包围盒 x 0.01~97.99,变换为单位阵 ⇒ 原点 = 板左下角(可直接套板坐标) - 刀片无旋转、平放,引用 PhoneRail_blade.SLDASM(实体文件) NOTES 两处更正: - §14 二次更正:坐标系取决于插入方式 —— AddComponent4 插的会重心归零, 用户 GUI 手工插的保留板左下角原点。动手前先 GetBox 看一眼,别假设。 - §19 新增:三个磁盘文件各是什么、底板零部件引用的是临时件(一清理就断链), 并写明新会话第一件事不要 CloseAllDocuments。 新建文件夹/(126 MB STEP/SLDASM)加进 .gitignore。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
20 KiB
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. 连接
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
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. 清理
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 读,别猜
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)
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 个。
⇒ 唯一可行的装配路径是三步:
LoadFile4导入 STEP(得到未保存的顶层装配体)Extension.SaveAs3另存成.SLDASM(见 §11)CloseAllDocuments(True)→ 新建总装 →AddComponent5(那个 .SLDASM 路径, …)
13. ✅ ToolsCheckInterference2 的 5 参形式已经对
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(p,'',0,0,0) |
刀片 x -45~45、底板 x -49~49 |
几何中心(被重心归零) |
| 用户在 GUI 里手工插入 | 底板 x 0.01~97.99 / y 0.01~97.99 |
板左下角(与 EDA 板坐标一致) |
⇒ 下表那套"原点 = 板左下角"的平移量,在手工插入的装配体里是直接可用的;
在 AddComponent4 插出来的装配体里要按 GetBox 现算。
动手前先 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
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,别传零部件元组
已验证可用的唯一形式(全量检查):
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 |
属性读/写 |
mu = app.GetMathUtility() # ⇐ 加括号
xform = mu.CreateTransform(arr16) # arr16 可能要封送成 VT_ARRAY|VT_R8
comp.SetTransformAndSolve2(xform) # 或 comp.Transform2 = xform
⚠ §2 那条"无参 = 属性"的规律有例外:GetMathUtility 无参却是方法。
⛔ 但加括号也不行 —— app.GetMathUtility() 照样报"找不到成员"。
动态派发对这个成员建的包装是坏的。绕过 getattr,直接按 dispid 调:
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)。
19. 📍 当前 SolidWorks 里的装配体(2026-09-12 交接点)
用户手工把两块板载入了同一个装配体(不是脚本做的):
活动文档:D:\DCDC\新建文件夹\3D_BASE-PCB_2026-09-12.SLDASM
├─ 3D_BASE-PCB_2026-09-12.step-1 底板(用户自己重导的 STEP,不是脚本那份)
│ 包围盒 x 0.01~97.99 y 0.01~97.99 z -6.30~13.17 mm
│ 变换 = 单位阵 ⇒ **原点 = 板左下角,与 EDA 板坐标一致**
│ 引用 %TEMP%\swx32108\IC~~\3D_BASE-PCB_2026-09-12.step.SLDASM ⚠ 临时件
└─ PhoneRail_blade-1 刀片
包围盒 x -34.57~55.40 y 33.71~101.68 z -2.35~4.02 mm
变换 = 无旋转 + 平移 (-34.6, 33.7, 2.4) mm ⇒ **还是平放的,没竖起来**
引用 D:\DCDC\新建文件夹\PhoneRail_blade.SLDASM ✅ 实体文件,可用
状态快照存在 .claude/r2/3d/_sw_assembly_state.json。
磁盘上的文件(D:\DCDC\新建文件夹\,126 MB,未进 git)
| 文件 | 是什么 |
|---|---|
3D_BASE-PCB_2026-09-12.SLDASM 29.97 MB |
装配体本身(活动文档) |
3D_BASE-PCB_2026-09-12.step 53.87 MB |
用户自己重导的底板 STEP |
PhoneRail_blade.SLDASM 16.61 MB |
刀片零部件 ✅ 实体,装配体引用的就是它 |
PhoneRail_blade.step 31.22 MB |
刀片 STEP |
~$*.SLDASM(各 6 B) |
SolidWorks 锁文件 ⇒ 这两份正被打开 |
⚠⚠ 底板零部件引用的是临时件(%TEMP%\swx32108\IC~~D_BASE-PCB_2026-09-12.step.SLDASM),
不是上表那个 29.97 MB 的装配体文件(那是装配体自己)。
⇒ 只要没 CloseAllDocuments、没重启 SolidWorks,引用就还在;
一旦清理或重启,重开装配体时底板零部件会丢引用。
⇒ 要长期可用:在 GUI 里把底板零部件也另存到 新建文件夹\ 下并替换引用
(⛔ 别用脚本的 SaveAs3,那个写出来是废件,见 §11)。
⚠ 新会话第一件事:别急着 CloseAllDocuments —— 那会清空临时目录、
让当前这个装配体的底板引用断掉。先确认用户是否已把底板另存。
下一步只差摆位
刀片要绕 X 轴 +90° 竖起,再平移到某个槽位。因为底板原点 = 板左下角,
§14 那张表的平移量可以直接用(dx = 4.00、dy = 槽心 y + 0.8、dz = Z0)。
卡点仍是 §17:CreateTransform 的 pywin32 封送没解决。
绕开的办法:在 GUI 里手工拖 + 加配合(比解封送快),或者继续攻 §17。