# 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 = `**(属性赋值,不是方法调用)。 ## 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 个)。 ⛔⛔ **更正(2026-09-12 晚):参数形式对了,但这个方法本身是死的,别用。** "不报错"当时只在**空装配体**上验证过 —— 从没验证它**能不能检出一个已知的干涉**。 实测:把刀片摆成**板体直接穿过底板板体**(两块 PCB 在 z 0.50~1.67 实打实体积重叠)的姿态, `ToolsCheckInterference2(0, EMPTY, False, pc, pf)` 仍返回 `None`、PComp / PFace **各 0 条**。 同一姿态下 `IInterferenceDetectionMgr` 报 **692 处、板对板 4915.9 mm³**。 ⇒ 它不抛异常、返回值看着就是个正常的"0 处干涉",拿它当判据会得出 **"刀片一路插到底都不碰"** 这种彻底相反的结论(本会话真的得出过一次,整轮扫描作废)。 ⇒ **判据教训**(同 §11 那条):一个检测类 API「调用不报错」**不等于它在工作**; 必须先喂它一个**已知必然为真**的样本,看它报不报。见 §20。 ## 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]`。 ⛔ **更正(2026-09-12 晚实测)**:那 9 个数是**列主序**,不是这里原先写的"行主序"。 绕 X 轴 +90°(刀片 y→底板 z、刀片 z→底板 −y)要写成 `[1,0,0, 0,0,1, 0,-1,0]`,写成它的转置 `[1,0,0, 0,0,-1, 0,1,0]` 会把刀片**倒插** (板往 −z 长,包围盒 z 变成 −47.99~19.99)。 ⇒ 两种都是合法旋转、都不报错,**只能靠回读包围盒判**;`place_and_probe.py --validate` 就是干这个的:在够不着的高度摆一次,看 box 的 z 是往上长还是往下长。 | 槽 | 槽心 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 返回)。 ### ⛔⛔ `AddComponent4` 要求**被插入的文档当时是打开的**(2026-09-13 坐实) 自己建的托盘零件,`SaveAs3` 返回 True、`err=0`、`os.path.exists` 为真、文件 85 KB —— **一切都对**,但 `AddComponent4(那个路径)` 恒返回 `None`。 唯一的差别是:我在插入前 `app.CloseDoc(零件标题)` 把它关掉了。 对照本会话**三次成功**的插入(STEP 临时顶层件 §15、风扇占位体 §32、Orion OD6025), 共同点都是**目标文档仍开着**。把 `CloseDoc` 去掉后,同一份文件立刻插入成功 (`AddComponent4 → Tray_v2-1`,装配包围盒精确命中期望)。 ⇒ **固定动作:建好零件 → `SaveAs3` → 别关它 → `AddComponent4` → 之后再关**。 ⚠ 这个坑的伪装性很强:它和「文件没落盘」(§34 heredoc 中文那条)**现象完全一样**, 都是 `AddComponent4 → None`。分辨办法是先 `os.path.exists` + 看文件大小 —— 文件在 ⇒ 是本条(没打开);文件不在 ⇒ 是落盘失败。 ## 16. ⚠ 干涉检查的第 2 参:只能传 `EMPTY`,别传零部件元组 > ⛔ 本节整节**已作废** —— `ToolsCheckInterference2` 是死的(§13 更正),改用 §20。 > 保留只为记住"两种口径都试过"这段弯路。 已验证可用的唯一形式(全量检查): ```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)。 ## 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。 ## 20. ✅ 干涉检查的**唯一可用**入口:`IInterferenceDetectionMgr` ```python idm = asm.InterferenceDetectionManager # 属性,不加括号 idm.TreatCoincidenceAsInterference = False # 面贴面不算干涉 idm.IncludeMultibodyPartInterferences = True idm.TreatSubAssembliesAsComponents = False # False ⇒ 报到**叶子零件**,能指名 idm.IgnoreHiddenBodies = True idm.UseTransform = False n = idm.GetInterferenceCount # ⇐ 属性形式 for it in idm.GetInterferences: # ⇐ 属性形式,返回 tuple o = wc.Dispatch(it) v_mm3 = o.Volume * 1e9 # Volume 单位是 m³ names = [wc.Dispatch(c).Name2 for c in o.Components] idm.Done() # 每轮都要调,否则结果会串 ``` - ⛔ `GetInterferenceCount` / `GetInterferences` 在 `sldworks.tlb` 里标 **invkind=1(方法)**, 但动态派发下**必须当属性取**(不加括号),同 §9。加括号报 `'int'/'tuple' object is not callable`。 - ⛔ `SetSelectedComponents` **不存在**(`GetIDsOfNames` 报"未知名称")。想限定范围用 `GetComponentsTransformInterference(Component, Transform, InterferingComponent)`(memid=18), 或者像下面那样**自己按子树过滤**。 - `IComponent2.Name2` 返回的是**全路径**(`顶层-1/子装配-1/叶子-1`), ⇒ 判断某个叶子属于哪块板,看路径前缀,不能看叶子名(`R0402~…` 两块板上都有)。 ### ⚠ 必须按子树过滤:底板自身就有几百处"预存干涉" EasyEDA 导出的 STEP 里,器件 3D 模型压进板体 / 相邻器件模型互穿是常态。 实测两块板**各自摆开、互不接触**时,全装配干涉数就已经是**几百**。 ⇒ 直接看 `GetInterferenceCount` 等于看噪声。只统计 **「一端在刀片子树、另一端在底板子树」** 的跨对,并且先在"刀片悬空"姿态下 确认跨对数 = 0 作为基线,再往下扫。 ## 21. ⚠⚠ STEP 里的 3D 模型**严重不全** —— 干涉结论的适用范围 实测(`GetBox` 递归到叶子,转回各自板的局部坐标): | | 3D 模型覆盖 | 实测最大凸出 | 设计值 | |---|---|---|---| | 刀片 A 面 | **一个都没有** | 0.00 | 手机主板 + `J4`(BM24) + `C60`(11) | | 刀片 B 面 | 75 个标准库件 | **4.70**(一个 1.25mm 2P 座)| — | | 底板 背面 | 有,但缺高件 | **6.30**(SRP1265A 电感)| 最高 12(C2/C3 直立电解)| | 底板 插槽面 | ✅ 插槽在 | 插槽体 **11.56 高**,顶面 z=**12.33** | 文档写"约 9" | ⇒ **能用干涉检查回答的**:舌头 / 台阶与插槽塑料体的配合、刀片板与底板板面的距离。 ⇒ **不能用干涉检查回答的**:刀片之间会不会打架、刀片与底板背面高件的关系 —— 缺的恰恰是最高的那些件。那些只能靠 `pcb_place.py` 里的高度预算算,别指望 3D。 另外:插槽 3D 模型的 **y 中心比焊盘中心偏 −0.45 mm**(五个槽一致), 而刀片板厚只有 1.67 —— 摆位时若按焊盘槽心对中,在模型里是偏心的。 ## 22. ⛔⛔ 自动化连接会**丢**:`GetActiveObject` 报 `-2147221021 操作无法使用` 本会话实测的完整现象与排查顺序(这一串每条都踩过): **先确认不是崩溃**(这三条同时成立 ⇒ 进程好好的,别急着重启): | 查什么 | 怎么查 | 本次实测 | |---|---|---| | 进程在不在 | `Get-Process SLDWORKS` | 在,`Responding=True` | | 忙不忙 | 采样 `TotalProcessorTime` 3 秒 | **0 ms**,完全空闲 | | 引用链断没断 | `ls %TEMP%\swx\IC~~\` | 584 个文件,**完好** | ⇒ **重启是最后手段**:`%TEMP%\swx\` 跟着 PID 走,一重启底板零部件的引用就断了(§19)。 **真因之一:SolidWorks 停在活动命令里时拒绝自动化。** 本次是干涉检查的 PropertyManager 没退出(脚本崩在 `AttributeError`, `finally` 里的 `idm.Done()` 只清了 COM 侧,GUI 侧命令还开着)。 ⇒ **最快的判据是截屏看状态栏左下角** —— 它当时写着「选择要检查干涉的零部件。」, 模型也还是干涉检查特有的半透明灰。Esc 之后状态栏恢复、着色恢复。 ⇒ ⚠ 但**退出命令后 ROT 注册并没有自己回来** ⇒ 活动命令不是唯一原因。 ### ⛔ `Dispatch` 真的会另起一个空实例(§1 那条警告已实测坐实) ``` 连接前 SLDWORKS PID: ['32108'] GetActiveObject 失败 → 改用 Dispatch 连接后 SLDWORKS PID: ['22388', '32108'] ⇐ 新进程 RevisionNumber = 33.1.1 文档数 = 0 ⇐ 空的,原装配体不在里面 ``` ⇒ **护栏写法**:连接前后各抓一次 PID 集合,比对有没有新增; 再用 `GetDocumentCount` / `ActiveDoc.GetTitle` 确认连的是不是那个装配体。 没有这层护栏,会对着一个空实例跑完整套脚本而毫无察觉。 ### 两个附带的 Windows 侧坑 - `Get-Process.MainWindowHandle` 给的**不一定是主窗口**(本次拿到的是 `SysShadow` 那个影子窗口,句柄 2692654,而真正带标题的是 `Afx:...` 类的 857392)。 ⇒ 要发按键就自己 `EnumWindows`,按「类名以 `Afx:` 开头 + 标题含 SOLIDWORKS」挑。 - PowerShell `Add-Type` 的内联 C# 里**不能有 `public static … Main(…)`** —— 会被当成程序入口点,报「签名错误,不能作为入口点」,整个类型编译失败。换个名字即可。 ## 23. 插槽配合的实测数据(2026-09-12,中间槽 J36) 刀片按「绕 X +90° 竖起、`dx=4.00`、板厚中面对焊盘槽心 y=49.00」摆入, 自上而下扫 `dz`(= 刀片底边 / 金手指尖的 z),只统计刀片↔底板跨对: | dz | 跨对数 | 干涉体积 | 谁碰谁 | |---|---|---|---| | 13.00 / 12.50 | 0 | 0 | — | | **12.00** | 2 | **0.466 mm³** | 刀片 `Board` ↔ `PCIE-64P11L` | | 二分 | 首次接触 **12.312**,恰好不干涉 **12.328** | | | 塑料体顶面 z = **12.33** ⇒ 表面看是「一点都没插进去,d = 0」。 ⚠ **但别就此下结论**,体积数对不上「平贴在实心顶面上」: - 若顶面是实心、刀片平贴:接触面积 = 舌头 36.0 × 板厚 1.67 = **60.1 mm²**, dz=12.00 时(压入 0.33)体积应是 **19.8 mm³**。 - 实测 **0.466 mm³**,只有 2.3%;反推接触面积仅 **1.41 mm²**,而且分成 **2 个区**。 ⇒ 这是**两处很局部的接触**,不是整条边平贴。两种候选解释,尚未分清: **(A)** 腔比舌头**短**,舌头两端顶在腔的两端壁上(1.41 mm² ≈ 两端各压入 0.25 mm 的量级) —— 若成立,说明**舌头 36.0 还是太宽**,正是用户待办 ⭐① 要量的那个数; **(B)** 腔是够的,但插槽 3D 模型的 y 中心比焊盘中心**偏 −0.45**, 1.67 的板按焊盘槽心对中就会蹭到腔口边缘。 ⇒ 分辨办法:在腔内高度做 **y 向扫描**找无干涉窗口((B) 会有明显窗口), 再做 **x 向扫描 / 换窄舌头**验证 (A)。两个都还没跑成。 ### §22 续:ROT 丢了之后,哪些路已经试死 | 试法 | 结果 | |---|---| | 等待重试 12 次 × 4 s(共 44 s) | 全失败,不是「忙一下」 | | Esc 退出干涉检查命令(**确认生效**:状态栏恢复、半透明着色恢复) | ROT **仍未回来** | | 枚举**整张运行对象表** `pythoncom.GetRunningObjectTable().EnumRunning()` | **0 条**(不是"没有 SolidWorks",是一条都没有)| | 带版本号 ProgID | 只有 `SldWorks.Application` / `.33` 存在(`.32/.31/.1` 报「无效的类字符串」),两个都是「操作无法使用」| | UAC / 会话隔离理论 | **排除**:两进程同用户、同会话 2、均未提权(客户端 Medium 完整性)| ⇒ **结论:注册一旦被撤销,客户端侧无论如何都连不回去**,只能靠 GUI 侧动作或重启 SolidWorks。 ⇒ **所以重点在预防**:脚本里任何异常都不能把 SolidWorks 留在活动命令里。 干涉检查尤其要 `try/finally` 保证 `idm.Done()`,并且**出异常后主动再 Esc 一次** (`idm.Done()` 只清 COM 侧,GUI 侧的 PropertyManager 不会跟着关)。 ### ⚠ 小坑:拿全精度值去比对"四舍五入过的快照" `_blade_xform_backup.json` 存的是全精度(`-0.03458581301934173`), 而 `_sw_assembly_state.json` 是**round 到 6 位**的(`-0.034586`)。 用 `1e-9` 容差比会判「不一致」——**是误报**。按快照的舍入位数定容差(这里 5e-6)。 ## 24. ⛔⛔ 更正 §22 的因果:丢 ROT 是**用了 IDM**,不是"崩在活动命令里" §22 当时把失联归因于"脚本崩溃时把 SolidWorks 留在了干涉检查命令里"。**这个因果是错的。** 第二次实测:`place_and_probe.py sweep` **`exit code 0` 干净跑完、没有任何异常** (`idm.Done()` 正常调用、兜底 except 根本没触发),跑完之后 `GetActiveObject` **照样**报 `-2147221021`,运行对象表**照样**枚举为 0 条。 两次失联的共同点只有一个:**重度使用了 `IInterferenceDetectionMgr`**。 而此前十几个短脚本(probe1~6,含一次轻量 IDM 调用)反复连接都没事。 ⇒ **实操结论**:把 IDM 当成"用完连接就会没"的一次性资源。 **凡是需要 SolidWorks 连接的事,必须和 IDM 调用放在同一个进程里一次做完**; 不要指望跑完 IDM 之后还能再连上去截图 / 读几何 / 收尾。 ⇒ 恢复只有一条路:`taskkill /F /IM SLDWORKS.exe` → 重启 → 重导 STEP(底板 ≈50 s)。 所以脚本最好像 `slot_cavity_test.py` / `slot_cavity_measure.py` 那样**自带自愈** (连不上就自己杀、自己起、自己重导),而不是假设连接还在。 ## 25. ⛔ `IBody2.GetFaceCount` 返回 **0**(假值),别拿它做判据 实测插槽零件 3 个实体,`GetFaceCount` 全部返回 `0` —— 实体不可能 0 个面。 用 **`len(body.GetFaces)`**(属性,返回 tuple)。 ### 由此引出的一个判据写法教训(比这个 API 本身更重要) `slot_cavity_test.py` 原来的判据是: ```python if r > 0.80 or faces < 30: # r = 实体体积/包围盒 print('实心块,没有槽腔') ``` 实测 `r = 0.492`(明明说"不是实心"),却被假值 `faces = 0` 满足的 `faces < 30` **一票翻盘**,打印出与证据相反的结论。 ⇒ **不要把强弱不同、可信度不同的信号用 `or` 并列**。 弱信号(尤其是可能返回假值的 API)只在**读到有效值时**才允许参与判断。 这与 §11 / §13 是同一类错误:**返回值看着正常 ≠ 它在工作**。 ## 26. ✅ 几何遍历:`GetFaces` 是**方法**,以及一个通用取值助手 `IBody2.GetFaces` 取回来是个 **python 方法对象**(`len()` 报 `object of type 'method' has no len()`),要**加括号**。这是 §9「0 参即属性」的 又一个例外(前一个是 §17 的 `GetMathUtility`)。 ⇒ 与其逐个记,不如用这个助手 —— 它只认 **python 侧**的方法类型, 因此不会犯 §2 那个「COM 对象一律 callable ⇒ 把子对象当函数调」的错: ```python def prop(o, name): v = getattr(o, name) if type(v).__name__ in ('method', 'function', 'builtin_function_or_method'): return v() # 动态派发把它包成了方法 return v # 属性(含返回 COM 子对象的情形) ``` 实测可用的遍历链(全部经 `prop()`): `part.GetBodies2(0, True)` → `body.GetFaces` → `face.GetSurface` → `surface.IsPlane` / `surface.PlaneParams`(前 3 个是法线)→ `face.GetBox`(米)/ `face.GetArea`(m²)/ `face.GetLoops` → `loop.IsOuter` / `loop.GetEdges`。 ## 27. 🎯 `PCIE-64P11L` 插槽模型的实测解剖(这决定了 3D 能回答什么) 零件本地坐标:包围盒 `x ±20.50 / y −4.39~4.96 / z −1.56~10.00`,**塑料体顶面 z = 10.00**。 **3 个实体**:两个 Ø 定位柱(各 4.8 mm³、26 面)+ 塑料壳(**1883 个面**、2172.8 mm³)。 体积/包围盒 = **0.492** ⇒ **是建了真结构的连接器,不是简化实心块** (⚠ 早前据假值 `GetFaceCount=0` 得出的"实心块"结论作废,见 §25)。 **触点通道**:z=3.00 处一排排 0.40×0.80 的面,**x 间距正好 1.00 mm**(= PCIe 脚距), 分两排在 y `−0.96~−0.16` 与 `1.64~2.44` ⇒ 两排中心 −0.56 / +2.04, **卡槽中心线 y = +0.74**。 **换算到装配坐标**:`assembly_y = part_y + 48.26`(由槽3 的 y 43.87~53.23 反推) ⇒ 卡槽中心线落在 **assembly y = 49.00 = 焊盘槽心**。 ⇒ ⛔ **此前怀疑的"插槽模型 y 偏心 −0.45"是误判** —— 那 −0.45 只是包围盒被 单侧卡扣台肩(y 到 +4.96)拉偏,卡槽本身正对焊盘中心。 ### 🎯 卡槽开口的准确尺寸(顶面内环,`slot_opening_measure.py` 实测) 顶面 z=10.000 一共 71 个环:外环 1 个(39.00 × 7.30)、触点窗口 64 个 (0.40 × 1.00,两排在 y −1.96~−0.96 与 2.44~3.44)、角窗 4 个, **卡槽开口 2 个** —— ``` 环0 x −17.540 ~ −5.390 (12.150) y −0.160 ~ 1.640 (1.800) 环1 x −4.610 ~ 17.540 (22.150) y −0.160 ~ 1.640 (1.800) └─ 中间 0.78 mm 实体 = PCIe 防呆键,中心 part x = −5.00 ``` ⇒ **开口全长 = ±17.540 = 35.08 mm,开口宽 = 1.80 mm,键中心 part x = −5.00。** 三条独立佐证(都对上了才敢用): 1. 开口 y 中心 = **+0.74** = 由触点两排(−0.56 / +2.04)推出的通道中心线; 2. `assembly_x = part_x + 49.00` ⇒ 键中心落在装配 x = **44.00**, 正是封装针位表 `_U` 里 `u −6.5 ~ −3.5` 那个键槽的中心(`SLOT_CX 49 − 5`); 3. `assembly_y = part_y + 48.26` ⇒ 通道中心落在装配 y = **49.00** = 焊盘槽心。 ### ⇒ 刀片舌头 36.00 **确实插不进**(0.92 mm 过宽) | | 值 | |---|---| | 插槽开口长 | **35.08** | | 刀片舌头(`pcb_blade.TONGUE_X0/X1` = 27.0/63.0)| **36.00** | | 差 | **每侧 0.46,共 0.92 mm** ⇒ 舌头两端压在端壁上,插不下去 | | 开口宽 1.80 vs STEP 板厚 1.67 | ✅ 厚度方向有 0.13 余量,没问题 | **与碰撞法互相印证**:干涉扫描在 dz=12.00(压入 0.33)测得体积 0.466 mm³, 反推接触面积 1.41 mm²;按实测过盈算应是 `0.46 × 1.67 × 2 = 1.54 mm²` —— 吻合。 ⇒ `d = 0.00`、`y ±1.6 全程干涉`、`x ±2.0 全程干涉` 都是**真实几何冲突**。 ⛔ **但注意判据的先后**:碰撞法本身**推不出**这个结论(§23 那段推理是无效的, 因为"腔比舌头短"和"模型没建腔"会给出完全相同的现象)。 是**先直接量到开口 35.08**,碰撞数据才获得解释力。**顺序不能倒过来。** ⚠ 这是**厂商 STEP 的标称几何,不含公差**。用户待办 ⭐① 的卡尺实测仍是权威口径; 本节的价值是给出了一个**具体待验证的数**(35.08)和明确方向(36.00 偏宽)。 ## 28. ⛔ 再更正 §24:丢 ROT **与 IDM 无关**,是长时间大量 COM 调用后普遍会丢 §24 把失联归因于"重度使用 `IInterferenceDetectionMgr`"。**这个因果也不对。** 第三次失联:重启后**全程没碰过 IDM**,连续跑了 8 个纯几何脚本 (`slot_cavity_test` / `slot_cavity_measure` ×2 / `slot_opening_measure` ×2 / `slot_depth_measure` / `rib`),每个都正常复用连接;到第 9 个 (`walls.py`)时开始返回空结果,第 10 个直接 `GetActiveObject` 失败。 ⇒ **实际规律**:SolidWorks 的 ROT 注册在**长时间、大量 COM 调用**之后会消失, 与调用的是哪类 API 无关。IDM 只是第一、二次碰巧在场。 ⇒ **实操结论不变、但理由要改**: **每个脚本都要能独立自愈**(连不上就 `taskkill` → `Dispatch` 重启 → 重导 STEP, 底板 ≈50 s),不要假设上一个脚本留下的连接还能用。 ### ⛔⛔ 连接将死时的表现:**不是报错,是静默返回空** `walls.py` 那一轮返回「通道内竖壁 0 个」,看上去像个正常的几何结论。 实际是连接已在失效、**每个面的 `GetBox()` 都在抛异常**,而被 `try/except: continue` 逐个吞掉,最后自然剩 0 个。 ⇒ 这是本会话第**三**次栽在同一类坑上(前两次:`ToolsCheckInterference2` 的假 0、 `GetFaceCount` 的假 0)。**统一对策**:任何"遍历 + 过滤 + 计数"的脚本, 必须打**分级计数器**(总数 / 取值成功 / 各级过滤后剩余)并记录首个异常, 这样"真的是 0"和"全都失败了"一眼可分。**不要让 `except: continue` 静默吃掉全部样本。** ### §28 附:连接将死的**确切表现**(有了硬证据) 给面遍历加上分级计数器之后,抓到了这样一轮: ``` 分级计数: {'total': 1883, 'boxok': 495, 'boxfail': 1388, 'zerox': 109, 'zband': 83, 'yband': 28} 首个 GetBox 异常: (-2147417848, '被调用的对象已与其客户端断开连接。') ``` **1883 个面里 1388 个取值失败**(`RPC_E_DISCONNECTED`),只有 495 个成功。 于是"通道两端壁"和"键筋两侧"这些**恰好落在失败那 74% 里**的面全部消失, 输出看上去是一个完整、干净、却完全错误的几何结论。 ⇒ 没有计数器时(上一版 `walls.py`),同样的情况输出的是 **「通道内竖壁 0 个」** —— 一个看着像答案的答案。 ⇒ **所以计数器不是锦上添花,是这类脚本的必需品。** 另外:`boxfail > 0` 就应当**直接停手**,不要拿残缺样本出结论。 ### ⚠ 这个插槽模型的精度上限:0.2 mm 级的配合判不了 同一个模型里有两处自相矛盾的数,说明厂商模型只在毫米级可信: - 簧片触点把通道夹到 **1.568**,比它要接纳的 1.6 标称卡还窄; - 防呆键筋宽 **1.78**(顶端 0.78),而 PCIe CEM 的键筋应在 1.5 附近。 ⇒ **判据边界**:用这个模型回答「差 0.9 mm 的开口长度够不够」是可以的; 回答「差 0.18 mm 的键筋/缺口配合」不行。**结论的可信度要按被测量的量级来定**, 不能因为"同一个模型量出来的"就一视同仁。 ## 29. ✅ 新版两块板的复核结果(2026-09-12 深夜,`3D_*-PCB_2026-09-12.step`) 用户重导的两份 STEP(底板 44.9 MB / 刀片 25.9 MB)实测,**收窄后的舌头装得进**: | 项 | 设计 | STEP 实测 | 判 | |---|---:|---:|---| | 刀片外形 | 89.5 × 67.0 × 1.6 | 89.47 × 66.97 × **1.67** | ✅ | | 舌头宽 | 34.50 | **34.474** | ✅ 对开口余 0.30/侧 | | 台阶高 | 9.9 | **9.900** | ✅ | | 防呆缺口 | 中心 40.0 / 宽 1.9 / 深 8.3 | x 39.037~40.963(**1.926**)/ 深 8.3 | ✅ | | 插槽开口 | — | **35.080** × 1.800,键中心 −5.000 | 与上一轮一致 | | 腔深 | — | **8.000**(顶面 10.660 − 腔底 2.660,零件本地)| — | | **插入深度 d** | 8.00 | **8.00 达成** | ✅ 刀片底边离板面 **2.66** | ### ⛔ 最重要的一条:**「首次接触」不是「机械止点」** dz 扫描的首次接触在 **9.296**(d 只有 3.03),照上一轮的读法会得出 「还是插不到底」。**错的**——那是 64 根**簧片触点**:模型把簧片建成刚性、 通道被夹到 1.568 < 板厚 1.67(§28),所以板一碰到簧片就报干涉,而实物里簧片是要被压开的。 **分辨办法(本轮定型,以后照抄)**:不看"有没有干涉",看**体积随深度的斜率**, 并且必做一个**阳性对照**(压到明知该撞的位置,确认检测在工作): | dz | 插入深度 | 跨对数 | 总体积 | 最大单块 | 读法 | |---|---:|---:|---:|---:|---| | 4.35 | 7.98(腔底上方 0.02)| 72 | **1.491 mm³** | 0.50 | 簧片蹭,塑料没挡 | | 3.80 | 8.53(压进腔底 0.53)| 70 | **28.243 mm³** | 18.20 | ← 阳性对照,检测确实在工作 | ⇒ 只有簧片蹭 ⇒ 卡能坐到腔底,**d = 8.00**。 ⚠ 别拿"跨对数"当判据:两行的跨对数几乎一样(72 vs 70),**只有体积分得开**。 ### ⛔ 量轮廓别用「面包围盒筛选」,要抽**底面外环的折线点** 第一版用「完全落在 y ≤ 台阶+0.3 的面」筛舌头,量出 **85.5**(假值)—— 被板四角的 **R2 圆角面**污染(圆角面的 y 跨度也很低)。 把台阶也量成 11.887(那其实是圆角外角点 (0.013, 11.887))。 ✅ 正确做法:取板体**法线 −Z 且面积最大**的平面 = 底面 → `GetLoops` → `IsOuter` 的外环 → 每条边 `GetCurveParams2` 取两端点 → 直接得到轮廓折线。 本例 22 条边 44 个点,y<14 的 18 个点**一眼就能读出凸字形**(舌头两端 27.763 / 62.237、 台阶肩 9.900、缺口两壁 39.037 / 40.963、45° 导入角 38.537→39.037)。 ## 30. ✅ 5 块刀片全插上:刀片之间不打架 `AddComponent4` 用**同一个路径连插 5 次**没问题(得到 `-1`…`-5`),逐个给变换即可。 | 项 | 值 | |---|---| | 干涉总数(fast 口径)| 360 = **5 × 72**,全是「刀片↔底板」的簧片蹭 | | **刀片↔刀片** | **0 处** | | 相邻刀片净距 | **13.950 mm**(间距 20.32 − 板 1.67 − B 面器件 4.70)| ⚠ 只对 B 面有效:刀片 **A 面在 STEP 里一个模型都没有**(§21), 手机主板 + `J4`/`C60` 那一侧的间距**这张图答不了**,仍要靠 `pcb_place.py` 的高度预算。 ## 31. API 增补与一条对 §22 的更正 - ✅ `doc.ShowNamedView2('', 7|1|4)` + `doc.ViewZoomtofit2`(属性)+ `doc.SaveBMP(路径, w, h)` 三件套可用,截图便宜 ⇒ **摆完先截图再干别的**(后面的干涉检查很可能把连接跑死)。 - ⛔ `IModelDocExtension.GetPackAndGo`(dispid **207**)**三条路全死**: 属性形式取到 python function 对象(再点属性报 `'function' object has no attribute`)、 加括号报"非选择性的参数"、`_oleobj_.Invoke` 的 PROPERTYGET / METHOD / 两者或 分别报"无效的参数数目"/"非选择性的参数"。§17 那招在这个成员上不灵。 ⇒ 要长期可用的装配体文件,改用 GUI 的「文件 → Pack and Go」。 ⚠ 由此:脚本 `SaveAs3` 出来的总装(本轮 `5blades\PhoneRail_5blades.SLDASM` 41.2 MB) 仍是 §11 那种**废件**——子件在 `%TEMP%\swx\IC~~\`,SolidWorks 一关就断引用。 - ⛔⛔ **更正 §22**:「`Dispatch` 一定会另起一个新实例」**不成立**。 它的真实行为是 **ROT 里有注册就附着到现有实例,没有才新建**。 本轮实测:前两次(用户实例 ROT 已死)确实新建了 17452 / 23272; 第三次用户自己重启过 SolidWorks、ROT 是活的,`Dispatch` 直接**连进了用户那个实例** (日志里 `新实例 [] 文档数 295` 就是这个意思)。 ⇒ 护栏照旧:连接前后比 PID 集合 + 看 `GetDocumentCount`,**并且要想清楚 "连进用户实例"是不是可接受**(本轮是可接受的:只新增文档,不动用户的装配体)。 - 一轮"重度 IDM"的代价实测:全量干涉一次 **155~163 s**(底板 245 处预存干涉参与计算), **连做两次之后连接必死**。fast 口径(`TreatSubAssembliesAsComponents=True`)只要 **4 s** 且天然只剩跨对 ⇒ **能用 fast 就别用全量**,全量只在需要指名叶子零件时打一发。 ## 32. ✅ 往装配体里加「本来没有模型」的件(风扇) 本机**没有任何 CAD 内核**(`cadquery` / `OCC` / `build123d` / `trimesh` 全没装,只有 `numpy` + `numpy-stl`)⇒ 要凭空造一个占位体,两条路: | 路 | 结果 | |---|---| | STL(numpy-stl 能写)| ⛔ 不走:SolidWorks 导 STL 默认是**图形网格体**,干涉检查对网格体不可靠 | | **手写 STEP AP214** | ✅ **可行且已验证**:`make_box_step.py` 生成 60×60×25 的 B-rep 实心块(152 个实体行),导入后 `GetBodies2(0,True)` = **1 个实体 / 6 个面 / 包围盒 0~60,0~60,0~25** | 手写 B-rep 的唯一要点:**共享边必须复用同一个 `EDGE_CURVE`**(12 条边被 6 个面两两共用, 靠 `ORIENTED_EDGE` 的 `.T./.F.` 定方向)。不复用就不是流形实体,会被导成一堆曲面。 ### ⛔ 单体 STEP 导进来同样**没有路径**,但它另存是安全的 `LoadFile4` 一个单体 STEP → 得到 type=1 的**未保存**零件,`GetPathName` 是**空串** (同 §10),于是 `AddComponent4` 无从引用(实测返回前就卡在"拿不到路径")。 ⇒ 先 `fan_doc.Extension.SaveAs3(路径, 0, 1, EMPTY, EMPTY, err, warn)` 存成真 `.SLDPRT`(61 KB), 再拿这个路径 `AddComponent4`。 ⚠ **这和 §11 的「另存出来是废件」不矛盾**:§11 坏在**装配体的子件仍指向 `%TEMP%`**; 单体零件**没有外部引用**,另存就是完整的。⇒ 判别标准是「有没有外部引用」,不是「用没用 SaveAs」。 (仍按 §11 的教训验一次:不看 `SaveAs3` 的返回值,看 `os.path.exists` + 文件大小。) ### 摆位:把零件本地轴转到想要的全局轴 风扇要沿 x 吹 ⇒ 本地 z(厚度 25)转到全局 x。列主序 9 元素 = 三个本地轴的像: `[0,1,0, 0,0,1, 1,0,0]`(本地 x→全局 y、本地 y→全局 z、本地 z→全局 x), 于是本地点 (a,b,c) → 全局 (c+tx, a+ty, b+tz),平移量直接按想要的包围盒反推。 ### 🎯 上哪儿弄机械件的 STEP:**Orion Fans 是唯一能脚本直下的**(2026-09-13 实测) 找 60×60×25 风扇模型时把主流渠道挨个试了一遍,结论对以后找任何机械件都适用: | 渠道 | 实测结果 | |---|---| | **orionfans.com** | ✅ **225 个 `.stp` 直接挂在站上**,形如 `https://orionfans.com/3d/OD6025_m0_frame.stp`,`curl` 直下(`application/step`,1.3 MB),**无登录无表单** | | 山洋 SANYO DENKI | ⚠ 要填表(`techcompass-lp.sanyodenki.com`),文件名 `9G_60x25_RBD`,人可以下、脚本不行 | | Noctua | ⛔ **HTTP 429**,WebFetch 和 curl 都被限流(站点级,不是鉴权) | | GrabCAD / Digi-Key | ⛔ **HTTP 403**(curl 直接被挡) | | Same Sky(原 CUI Devices)| ⛔ 页面可达(200),但 3D 是 JS 组件,HTML 里**没有任何 `.stp` 直链**,还挂着 `/cad-model-terms` | | TraceParts / 3DContentCentral / partcommunity | ⛔ 一律要登录 | | Mouser | ⛔ 连站点根都 403 | ⇒ **先试 Orion**;要别的品类时,判据是「HTML 里能不能 grep 出 `href=".../xxx.stp"`」。 ⚠ 本机 `curl` 有网(`raw.githubusercontent.com` 200),但 `gh` **没装**,GitHub 代码搜索这条路走不通。 ### ⛔ 删零部件:`EditDelete` 是**假成功**,要用 `Extension.DeleteSelection2(0)` 一次删两只占位体,踩到三层坑,每层的表现都是「看着成功了」: | 写法 | 现象 | |---|---| | `SelectByID2(nm,'COMPONENT',0,0,0,False,0,**None**,0)` | ⛔ 报「类型不匹配,**参数 8**」——第 8 参 `Callout` 是对象参数,要传 `VARIANT(VT_DISPATCH,None)`(同 §11 的 `SaveAs3`)| | `SelectByID2(..., **EMPTY**, 0)` | ⚠ **返回 `False`,但其实选中了** —— `doc.SelectionManager.GetSelectedObjectCount2(-1)` = **1**。⇒ **别拿它的返回值当判据**,要看选中数 | | `doc.EditDelete`(属性形式) | ⛔ **不报错、也不删**。脚本打印了"删除占位体 ×2",回读装配树两只**原样还在**,干涉里也照旧出现(`placeholder ↔ OD6025` 12256 mm³)| | **`doc.Extension.DeleteSelection2(0)`** | ✅ 返回 True 且**回读验证**确实删掉(顶层 10 → 8,残留 0)| ⇒ 这是本 skill 第**四**次栽在「返回值看着正常 ≠ 它在工作」上(前三次:§11 `SaveAs3`、 §13 `ToolsCheckInterference2`、§25 `GetFaceCount`)。 **固定动作:删除 / 另存 / 检测这类操作,一律用"回读装配树 / 回读文件"验证,不信返回值。** (这次正是靠"干涉报告里出现了本该已被删除的件"把假成功揪出来的。) ### 🎯 厂商模型的可信边界:Orion `OD6025` 实测解剖 单体、**203 个面**,包围盒 `±31.477 × ±31.477 × 0~25`。 | 量 | 实测 | 判 | |---|---|---| | 安装孔 | 4 个 **R2.150**,轴心 **(±24.999, ±24.999)**,z 贯穿 0~25 ⇒ **孔距 50.00 × 50.00 / Ø4.30** | ✅ 标准 60 mm 风扇 | | 风道 | 圆柱面 **R30.5**(Ø61)| — | | 框体外缘 | **62.954 见方** | ⚠ 比 60 标称**每边大 1.477** | ⇒ **这份模型只对「安装接口」可信**:外壳螺孔按 **50×50** 照抄没问题, **开孔轮廓必须按标称 60×60 留间隙**,不能照抄 62.95。同 §28 那条「按被测量的量级定可信度」。 #### ⛔ 量孔距时的判据陷阱(差点又出一个假结论) 脚本先按「圆柱面 + 半径 1.6~2.8 + 轴线沿 z」筛出 **9** 个候选,再对 `|x|`、`|y|` **各取最小值配对**,打印出 **「孔距 50.00 × 34.98」** —— 假的。 因为混进了一个 `(−30.253, 17.486)` 的 `R1.829`、只有 `z 0~3` 的杂孔(线夹/铭牌孔)。 ⇒ **配对前先按"四孔应关于原点对称 + z 贯穿全高"筛**,别对两个互不相关的集合各取极值。 这类"分别取极值再配对"的写法,在任何有杂散样本的几何统计里都会给出**看着合理的假数**。 ## 33. ✅✅ 原生特征建模打通(草图 + 拉伸),外壳不必用手写 STEP 2026-09-13 实测:**能用 API 建可编辑的原生 SolidWorks 特征**,回读包围盒精确等于设计值。 ⇒ §32 那条手写 STEP 只适合"本来就没有模型的外购件占位体";**自己设计的零件走这条链路**。 ```python tmpl = app.GetUserPreferenceStringValue(8) # → …\templates\gb_part.prtdot app.NewDocument(tmpl, 0, 0.0, 0.0); part = app.ActiveDoc part.Extension.SelectByID2('前视基准面', 'PLANE', 0,0,0, False, 0, EMPTY, 0) sm = part.SketchManager sm.InsertSketch(True) # 进草图 sm.CreateCornerRectangle(0,0,0, 0.050,0.030,0) # 6 参,单位**米**,返回 4 条线 sm.InsertSketch(True) # 再调一次 = 退出草图(toggle) part.ClearSelection2(True) part.Extension.SelectByID2('草图1', 'SKETCH', 0,0,0, False, 0, EMPTY, 0) feat = part.FeatureManager.FeatureExtrusion3( True, False, False, # Sd(单向), Flip, Dir 0, 0, # T1,T2 = swEndCondBlind 0.0024, 0.0, # D1 = 2.4 mm, D2 False, False, False, False, 0.0, 0.0, False, False, False, False, True, True, True, # Merge, UseFeatScope, UseAutoSelect 0, 0.0, False) # T0 = 从草图基准面开始 # → 返回 '凸台-拉伸1';回读 GetBodyBox = [0,0,0, 50,30,2.4] ✅ ``` 要点与坑: - **基准面在中文界面下叫 `前视基准面` / `上视基准面` / `右视基准面`**,别用英文名。 稳妥做法是遍历 `FirstFeature`→`GetNextFeature` 挑 `GetTypeName2 == 'RefPlane'`; 草图特征则是 `GetTypeName2 == 'ProfileFeature'`。 - `FeatureExtrusion3` / `FeatureExtrusion2` 都是 **23 个参数**(从 tlb 读的,别猜): `Sd, Flip, Dir, T1, T2, D1, D2, Dchk1, Dchk2, Ddir1, Ddir2, Dang1, Dang2, OffsetReverse1, OffsetReverse2, TranslateSurface1, TranslateSurface2, Merge, UseFeatScope, UseAutoSelect, T0, StartOffset, FlipStartOffset` - ⚠ **同名不同接口**:tlb 里 `IModelDoc.InsertSketch` 是 **0 参**,而 `ISketchManager.InsertSketch(UpdateEditRebuild)` 是 **1 参**。查签名时要连接口名一起看。 - ⚠ `SelectByID2(..., **'PLANE'** / **'SKETCH'**, ...)` 这两类**返回 `True`**, 而 §33 里 `'COMPONENT'` 返回 `False` 却其实选中了 ⇒ 返回值的可信度**按类型而异**, 统一的判据仍是 `SelectionManager.GetSelectedObjectCount2(-1)`。 - ⛔ **`GetBodies2(0, True)` 对"还没有实体的空零件"返回 `None`,不是空元组** —— 直接 `len()` 会炸。一律写 `part.GetBodies2(0, True) or []`。 - 收尾用 `app.CloseDoc(标题)` 关掉临时零件;⛔ 永远不要 `CloseAllDocuments`(§19)。 ## 34. ⛔ 基准面的草图轴映射:**必须按面记,不能想当然** 第一版托盘整块躺错了轴 —— 画的是 106×106 的托盘,回读却是 `x −4~102 / y 0~2.4 / z −102~4`(106×106 落在 **x-z 平面**、厚度沿 **y**)。 两次实测(都有回读包围盒作证)得到: | 基准面 | 草图 (u, v) → 模型 | 拉伸法线(正方向)| 证据 | |---|---|---|---| | **前视基准面** | (x, y) | **+z** | 50×30 矩形拉 2.4 → box `[0,0,0, 50,30,2.4]` | | **上视基准面** | (x, **−z**) | **+y** | (−4,−4)~(102,102) 拉 2.4 → box `x −4~102 / y 0~2.4 / z −102~4` | | **右视基准面** | (**−z**, y) | **+x** | 50×30 矩形拉 2.4 → box `x 0~2.4 / y 0~30 / z **−50~0**` | ⇒ 三个面里**有两个的草图横/竖轴会映射成模型的负轴**(上视的 v→−z、右视的 u→−z)。 要在右视基准面上按模型 z 坐标画竖板(法线沿 x),草图横坐标必须取 **−z**, 直接写 z 值画出来是上下颠倒的。 ⇒ 要让零件「平铺在 XY 平面、厚度沿 z」(也就是和 PCB 板坐标同向),画在**前视基准面**上。 ⚠ 这与直觉相反(名字叫"上视"的那个反而不是俯视平铺),**所以这一格必须查表,不能猜**。 ### ⛔ 反向拉伸要用第 3 参 `Dir`,`Flip`(第 2 参)对凸台无效 想让支柱朝底板的另一侧长,第一版传了 `Flip=True`(第 2 参)—— **实测无效**:柱子仍与底板同向长(box 从 0 长到 15,和底板 0~2.4 同侧,还被 `Merge` 合并成一个实体)。 `Flip` 是"反转切除侧",对凸台不起作用。⇒ **反方向用第 3 参 `Dir`(ReverseDirection)**。 ### ⛔ 切除是 `IFeatureManager.FeatureCut4`,**27 参**(同名的 IModelDoc 版只有 17 参) 第一次调用照抄了 `FeatureExtrusion3` 的 23 个参数 ⇒ 报「非选择性的参数」(= 参数个数不够)。 去 tlb 查时又踩了第二层坑:**扫描器只记录第一个同名项**,把 `IModelDoc.FeatureCut4`(17 参) 当成了答案。⇒ **扫 tlb 必须按接口名过滤**,同一个方法名在不同接口下签名完全不同: | 接口.方法 | 参数 | |---|---:| | `IModelDoc.FeatureCut4` | 17 | | **`IFeatureManager.FeatureCut4`** | **27** | | `IFeatureManager.FeatureCut3` | 26 | | `IFeatureManager.FeatureCut2` | 23 | | `IFeatureManager.FeatureCut` | 20 | 实测可用的 27 参调用(在柱顶面上画 4 个圆,往下切 6 mm 的 M3 热熔螺母孔): ```python fm.FeatureCut4(True, False, False, 0, 0, M(6.0), 0.0, False, False, False, False, 0.0, 0.0, False, False, False, False, False, # …, NormalCut True, True, False, False, False, # UseFeatScope, UseAutoSelect, 装配体三参 0, 0.0, False, False) # T0, StartOffset, FlipStartOffset, OptimizeGeometry # → 返回 '切除-拉伸1' ``` - ⚠ **按坐标选面会静默撞到邻面** —— 返回 True、选中数 1,但选中的根本不是你要的那个。 实测两例: - 托盘面板 z=0 处:想选朝上的面板顶面,选中的是**法线 (0,0,−1) 的底面** (那里压根没有朝上的自由面,因为底板是从 z=0 往 −z 拉、支柱又从 z=0 往 +z 长); - 端板 L 脚:在 (16.4, 51.345, −7.6) 想选脚顶面,选中的是 `box=[2.4,−4,−7.6, 2.4,102,72]`、**法线 (1,0,0)** 的板内侧立面。 ⇒ **选面一律遍历 `GetFaces` 按法线 + 面积 + 位置筛**,选完**回读 `PlaneParams` 与包围盒确认**, 再 `face.Select4(False, EMPTY)`。别用坐标去撞。 ⇒ 连带教训:`SelectByID2` 的返回值和选中数**都不能证明选对了对象**,只能证明"选中了某个东西"。 - `ISketchManager.CreateCircleByRadius(XC, YC, Zc, Radius)` 是 **4 参**(IModelDoc 的同名是 2 参)。 ### ⛔⛔ 含中文路径的脚本不要用 bash heredoc 跑,要用 Write 落成 .py heredoc 传输会**打碎脚本里的中文与反斜杠**:实测路径变成 `D:\DCDC\u65b0建文件夹\x05blades\...`,随后 `SaveAs3` 返回 **err=1**、文件根本没落盘, 而前面的建模步骤全部成功 —— 于是现象是「零件建得好好的,最后一步莫名其妙存不下来」。 ⇒ 本 skill 里凡是路径带 `新建文件夹` 这类中文的脚本,一律 **Write 成 .py 文件再运行**。 纯 ASCII 的只读探测脚本用 heredoc 没问题。 ⚠ 同一个坑还会伪装成别的样子:`AddComponent4` 返回 `None`(因为它引用的文件压根不存在)。 ⇒ **另存之后立刻 `os.path.exists` + 看文件大小**,别把"返回 True"当落盘成功(同 §11)。 ### ⛔⛔ 包围盒断言**对通孔完全没有分辨力** —— 两次独立事故坐实 `check(期望包围盒)` 能抓住"躺错轴""方向反了",但**打孔不改变外形尺寸**, 所以孔有没有真的打出来,包围盒一律给 ✅。本会话因此连续两次得到假结论: | 事故 | 包围盒断言 | 真相(换判据后) | |---|---|---| | 托盘端板锁孔 | ✅ 通过 | `FeatureCut4 → None`,Ø4.2 圆柱面只有 **4** 个(期望 8)⇒ 4 个锁孔一个没打 | | 端板全部孔 | ✅ 三步全过 | 零件**总共只有 8 个面、圆柱面 0 个** ⇒ Ø60 风口 + 4 风扇螺孔 + 2 脚锁孔**全部没成形**,就是块光板 | ⇒ **固定动作:凡是打孔/开口,判据必须是「圆柱面计数」而不是包围盒。** ```python rs = {} for f in prop(body, 'GetFaces'): s = prop(D(f), 'GetSurface') if prop(s, 'IsCylinder'): r = round(list(prop(s, 'CylinderParams'))[6] * 1000, 2) rs[r] = rs.get(r, 0) + 1 # 每个通孔 = 1 个对应半径的圆柱面;数目对不上就是没打成 ``` ⚠ 这和 §13(`ToolsCheckInterference2` 的假 0)、§25(`GetFaceCount` 的假 0)、 §33(`SelectByID2` 返回 False 却选中了)是同一族问题:**判据要能区分"成功"与"什么都没做"**。 ### ⛔⛔ `FeatureCut4` 在本机**极不可靠**:能不用就不用,改「孔画进轮廓草图一次成形」 统计本会话的全部调用: | 场景 | 结果 | |---|---| | 托盘**柱顶面**(实体的面上建草图)打 4 个 Ø4.2 | ✅ 唯一成功的一次,返回 `'切除-拉伸1'` | | 托盘面板 z=0(按坐标选面 → 选中的是**法线 −z 的底面**)| ⛔ None(两个方向都试) | | 托盘面板(改用**前视基准面**草图)| ⛔ None(两个方向都试) | | 端板 Ø60 风口 / 4 风扇螺孔(**右视基准面**草图)| ⛔ None,成品是块光板 | | 端板 L 脚顶面(面草图,与成功那次同款)| ⛔ None | ⇒ **首选做法:把孔和外框画进同一张草图,一次 `FeatureExtrusion3` 成形。** 实测可行(矩形 + 内部圆 → 拉伸后圆柱面数正确),端板的 Ø60 风口 + 4 螺孔就是这么出来的。 ⚠ **前提:圆必须完全落在外框内部**。托盘第一版锁孔放在 x=−2.0、半径 2.1 ⇒ 圆占 x −4.1~0.1 **越过面板边界 −4** ⇒ 轮廓自相交,`FeatureExtrusion3` 直接返回 `None`、 **整个零件建不出来**(回读 box 为 None)。孔心离边至少要留 `半径 + 壁厚`。 ### ⛔⛔⛔ 画草图前必须 `sm.AddToDB = True`,否则**自动捕捉会静默改掉你的几何** 这是本 skill 里最阴险的一个坑:`CreateCircleByRadius` 返回了对象、草图里 segment 数也对、 类型也对(4 线 + 4 圆),**但圆的坐标和半径已经被 SolidWorks 的草图推理改掉了**。 实测对照(同一份代码,只差 `AddToDB`): | 传入 | `AddToDB = False`(默认)| `AddToDB = True` | |---|---|---| | `(1.0, 21.345) r=2.1` | **`(0.000, 21.345) r=3.100`** ← 圆心吸到原点纵轴、半径 +1.0 | `(1.000, 21.345) r=2.100` ✅ | | `(1.0, 81.345) r=2.1` | **`(0.000, 81.345) r=3.100`** | `(1.000, 81.345) r=2.100` ✅ | | `(97.0, 21.345) r=2.1` | `(97.000, 21.345) r=2.100`(离参考线远,侥幸没被吸)| ✅ | ⇒ 被改大的圆要么尺寸错,要么**越出外框边界让整个轮廓失效**, `FeatureExtrusion3` 于是返回 `None`、零件建不出来 —— 而现象看起来像"拉伸 API 有问题"。 我为此先后误判成「破边」「一次成形不可用」「FeatureCut4 不可靠」,绕了三大圈。 ```python sm.InsertSketch(True) sm.AddToDB = True # ← 关掉全部自动捕捉/推理,几何按精确坐标写入 ...画线画圆... sm.AddToDB = False # ← 记得关回去 sm.InsertSketch(True) ``` ⚠ `app.SetUserPreferenceToggle(11, False)` **不是**这个开关,实测设了照样被吸。 ⚠ **固定动作:画完立刻回读每个圆的 `GetRadius` / `GetCenterPoint2` 比对**, 不要只数 segment 条数 —— 条数正确、几何错误,正是这个坑的伪装方式。 ### ⛔ 一连串"静默失败"的幕后黑手:**提交量(commit charge)耗尽**,不是物理内存 症状:`SaveAs3` 返回 **err=1** 却"文件存在"(其实是旧文件没被覆盖)、 后续脚本**卡在第一个 COM 调用上毫无输出**。 真因是 SolidWorks 弹了个**模态对话框**在等人点: ``` 警告!您的系统内存极低。执行此命令可能会导致 SOLIDWORKS 失败。 是否要继续?SOLIDWORKS 极力推荐您不要继续。 [是] [否] ``` 而"内存极低"指的**不是物理内存**(当时物理还空着 9.3 GB),是**提交量**: | 量 | 值 | |---|---| | 提交量 / 上限 | **36.5 / 38.0 GB(96%)** | | 物理内存 | 31.5 GB,可用 9.3 GB | | 页面文件上限 | **只有 6.6 GB**(当前才用 132 MB)⇒ 提交上限 = 31.5 + 6.5 | | SolidWorks 自身提交量 | **9.0 GB**(600 个文档,其中 584 个是 STEP 导入的临时子件)| **诊断三件套**(COM 卡住时按顺序做,别急着重启): 1. `Get-Process SLDWORKS` → 进程在、`Responding=True`、CPU 采样 **0 ms** ⇒ 不是崩溃、不是在算; 2. **枚举可见窗口找 `#32770`**(Windows 标准对话框类)—— 主窗口 `enabled=False` 就是被模态框挡住了; 3. **先读对话框文字再决定点什么**(`EnumChildWindows` 取子控件文本), ⛔ 别盲点确认 —— 有可能替用户选了"覆盖/丢弃修改"。 **清理办法**:⛔ 不能 `CloseAllDocuments`(会清空 `%TEMP%\swx\IC~~\`、废掉总装引用,§19)。 要**点名关**废弃的临时零件与装配体;关掉 7 个之后 SolidWorks 提交量 9.0 → **6.5 GB**、 总量 96% → **88%**,`SaveAs3` 立刻恢复正常(err=0、文件真的写出来了)。 清完**立刻复验**总装每个零部件的 `GetBox` 读不读得出来(读不出 = 引用断了)。 ### ✅ 反证:`FeatureCut4` 的失败**与内存无关** 提交量降到 88% 之后再跑,`FeatureCut4` **第 7 次返回 `None`**(同一处脚锁孔)。 ⇒ 上面那条"内存导致静默失败"**不适用于它**,前面关于它不可靠的结论维持不变: 能用「孔画进轮廓草图一次成形」就别用切除特征。 ### ⛔ 外壳件的落脚面:先量「托盘露在 PCB 之外的环」,别拿托盘外形尺寸当可用面 端板 L 脚抬到托盘面板上表面(z 0~2.4)之后,反而撞上了**底板 PCB**:3 处、最大 **468 mm³**。 原因是托盘面板顶面 z=0 **正是 PCB 的底面**,而 PCB 本体占 `x 0.01~97.99 / z −6.30~13.17` —— 脚只要伸进 x>0.01 就压在板子上。 实测可用的落脚环(回读两个零部件包围盒之差): | | 托盘 | PCB | 可落脚环宽 | |---|---|---|---| | 西侧 x | −4.00 | 0.01 | **4.01 mm** | | 东侧 x | 102.00 | 97.99 | **4.01 mm** | ⇒ **连带约束**:托盘上给端板用的锁孔必须整个落在这 4.01 的环里。 原来打在 x=1.0 / 97.0 的孔**全在 PCB 底下**,螺丝根本拧不进去; 而环中线 x=−2.0 配 Ø4.2 的孔(半径 2.1)又会与托盘外边相切/破边。 ⇒ 孔径降到 **Ø3.4**(M3 过孔)、孔心 **x=−1.9 / 99.9**:离托盘边 2.3、离 PCB 1.7,两侧都有料。 ⚠ 教训:**「装在托盘上」不等于「托盘的整个面都能用」**——PCB 自己占着中间那块。 画任何搭接件之前,先把相邻件的包围盒读出来相减,不要凭外形尺寸推。 ### ⛔ 旋转后再平移:对齐基准要按**语义**选,不能一律用 box 最小端 两块端板用同一个零件、东端绕 y 轴 180°。若统一按「目标最小值 − 旋转后最小值」平移, 东端板会落到 `x 123.00~153.40` —— **L 脚朝 +x 伸出机外**。 因为旋转 180° 后,"本体"从 box 的最小端跑到了**最大端**。 ⇒ 平移量要按你真正想对齐的那个**面**来算: 西端板用 `want_x − box[0]`,东端板用 `(want_x + 板厚) − box[3]`。 ⚠ 判据仍是回读 box 并与期望值比对,旋转件尤其要比**两端**,不能只看一端。 ### ⚠ 圆柱面计数这个判据本身也会被写错:**不同孔径必须分开数** 托盘上有两种孔:柱顶热熔螺母孔 **Ø4.2** 和端板锁孔 **Ø3.4**。 我把判据写成「Ø3.4 圆柱面应有 **8** 个 = 4 柱顶 + 4 端板」—— 柱顶那 4 个根本不是 3.4,于是永远只数到 4, 把**已经正确成形**的锁孔误判成"没成形",脚本还据此提前 `exit` 了,零件没落盘。 ⇒ 正确写法是**打直方图、按半径分桶各自比对**: ```python hist = {} # {半径mm: 面数} ... n_end = hist.get(3.4/2, 0) # 端板锁孔,期望 4 n_post = hist.get(4.2/2, 0) # 柱顶孔, 期望 4 ``` 修正后实测 `{1.7: 4, 2.1: 4, 4.0: 4}`(4.0 是支柱外圆),两类孔各 4 个,全部正确。 ⚠ 这是本会话第二次栽在"判据本身有毛病"上(前一次是包围盒对通孔没有分辨力)。 **换了更强的判据之后,还要检查这个判据的参数有没有写对** —— 否则只是把 "看不见的失败"换成了"看得见的假失败"。 ### ⛔ 180° 翻转要选对轴:**绕 y 会连带翻 z,把底部特征甩到顶部** 两块端板共用一个零件,东端要翻个面。零件本地:本体 `x 0~2.4 / z −10~72`, L 脚 `x 2.4~27.4 / **z 0~2.4**`(脚在底部)。 | 旋转 | 结果 | |---|---| | 绕 **y** 180°(`[−1,0,0, 0,1,0, 0,0,−1]`)| ⛔ x 翻的同时 **z 也翻**;再按"z 最小端对齐"平移 dz=+62,**脚被甩到 z 59.6~62**,正好扎进风扇腰部(风扇 z 6.37~69.33)⇒ 实测干涉 **483.9 mm³** | | 绕 **z** 180°(`[−1,0,0, 0,−1,0, 0,0,1]`)| ✅ z 不翻,脚留在底部 z 0~2.4,从风扇**正下方**掠过(2.4 < 6.37);x 翻使脚朝机内伸 | ⚠ 绕 z 翻转会**镜像 y**:本零件的风口中心在 y=51.345 而板的几何中心在 y=49(不对称), 翻转后风口跑到 −51.345 ⇒ 必须平移 **2×51.345 = 102.69** 才能对回风扇中心, 不能再用"box 最小端对齐"。附带好处:锁孔 y 21.345/81.345 互换成 81.345/21.345, **与托盘孔位仍然重合,托盘不用改**。 ⇒ **通用做法**:翻转件先只转不移、回读 box,**逐轴确认哪些轴被翻了**, 再按"你真正想对齐的那个面/那个特征中心"分轴算平移量 —— 不要三个轴统一套一个公式。 ## 35. ✅ 边缘圆角:`FeatureFillet3`(14 参)可用,但**只能喂给它做得出来的边** 签名(按接口名读,同名的还有 7 参 / 11 参两个旧版): | 接口.方法 | 参数 | |---|---:| | `IFeatureManager.FeatureFillet` | 7 | | `IFeatureManager.FeatureFillet2` | 11 | | **`IFeatureManager.FeatureFillet3`** | **14** | ```python feat = fm.FeatureFillet3(195, M(3.0), 0, 0, 0, 0, 0, None, None, None, None, None, None, None) # 195 = 简单等半径 ``` ### ⛔ 失败的真因:混进一条做不出的边,**整批放弃且不报错** 最小实验(纯长方体、12 条边)倒 4 条竖边 → 成功,直方图 `{3.0: 4}`。 但真实零件上一律返回 `None`: | 零件 | 选中 | 结果 | 混进来的坏边 | |---|---|---|---| | 托盘(60 条边)| 12 条 | ⛔ None | USB-C 缺口的内角边 —— 缺口只有 4.2 深,R3 在窄槽内角会自相交 | | 端板(34 条边)| 8 条 | ⛔ None | L 脚与板面的交线,同理超出可行范围 | ⇒ **三条对策**(v2 脚本,实测 5 个零件各 4/4 条全成): 1. **只倒外轮廓的角**:按实体包围盒筛,边的两个端点必须都落在包围盒角上 (`at_corner()`),自动排除所有缺口/开口/台阶产生的内部边; 2. **逐条单独倒**:一条失败不拖垮其余,还能精确报出是哪条边做不出来; 3. **失败自动降半径**:R3 → R2 → R1.5 → R1 依次重试。 ⚠ 判据仍是**圆柱面半径直方图**——包围盒对圆角没有分辨力(外形尺寸不变), 和"通孔"那次是同一个坑。 ### ⛔ `SaveAs3` 返回 err=1 的**第二种**诱因:同名文件正被打开 §34 记过一次 err=1 是**提交量耗尽**。这次又遇到 err=1,但排查下来完全不同: | 查什么 | 本次实测 | |---|---| | 提交量 | **29.7 / 37.0 GB(80%)**,物理还空 12.3 GB ⇒ **不是内存** | | 盘上文件时间戳 | `EndPlate_v4` 停在倒角那一步写入的时刻,**没被写坏**(回退版完好)| | 锁文件 | **`~$EndPlate_v4.SLDPRT` 存在** ⇒ 该文档正开在 SolidWorks 里 | ⇒ 真因是**同名覆盖 + 文档被自己占用**(脚本为了 `AddComponent4` 一直让零件保持打开)。 **换新文件名立刻 err=0**。 ⇒ 固定动作:改几何就**换版本号**,别覆盖正开着的文件; 并且永远用 `os.path.exists` + **时间戳** 判落盘,不看返回值。 ### ⚠ 外壳件上的阵列特征,基准要取**外壳自己的中心**,不是 PCB 的 顶盖第一版的泄压缝写成 `x 5~93`(8 条)——那是**底板**的坐标范围。 可顶盖跨 `x −27.4~125.4`、中心在 49,于是缝整片偏在东半侧, 西端一大片顶盖是实心的:气流短路、外观也不对称。**几何断言全过,是俯视图看出来的。** ⇒ 改成按盒子中心对称(11 条、跨度 106、中心 49)后才正常。 ⇒ **规律**:外壳件的尺寸基准来自「整机包围盒」,而 PCB 上的坐标只适用于板内特征 (安装孔、接口开口这些**要对准板上实物**的)。两套基准混用是这一步最容易犯的错, 而包围盒/面数这类判据**完全抓不到**——只能靠截图人眼看。 ### ⛔⛔ 干涉为零 **≠** 装配成立:还要单独验「该贴的面贴上了没有」 外壳五件干涉查出来一直是 360 基线、"零冲突",看着很完美。 但把两两包围盒拿来算**贴合面**时发现:`Tray ↔ SouthPanel` 和 `Tray ↔ NorthPanel` **一处接触都没有** —— 托盘 y −4~102,而南板在 −4.055~−1.66、北板在 104.345~106.745, 两块板整个落在托盘范围之外,**悬空**,螺丝无处可打。 ⇒ 干涉检查只回答"有没有撞",**不回答"该碰的碰上了没有"**。 凡是靠面接触定位/锁螺丝的装配,必须**另做一次贴合面检查**: ```python # 两件在某轴上 |a.max − b.min| < 0.05,且另两轴有重叠 ⇒ 贴合,重叠矩形就是可打螺丝区 ``` ⚠ 还要看**重叠区够不够宽**:本例 `南板↔顶盖` 虽然贴合,但重叠只有 **y 2.3 mm**, 打不下 M3 沉头,得先加凸台把贴合面加宽。 ⚠ 这个坑是"改一处带崩一处"的典型:端板改 y 对称 → 南北板跟着外移 → 托盘没跟着改。 **每次挪动件的边界,都要重跑一次贴合面检查**,别只看干涉。 ### ⛔ `ShowNamedView2` 的视图 id 与直觉完全不符 —— 别猜,用视线方向反查 想拍"正对南面"的图确认 Type-C 开孔,按「1 前 2 后 3 左 4 右 5 上 6 下」试了四次, 拍出来分别是俯视 / 北面 / 东端 / 仰视,**一次都没对**。 ⇒ 枚举 0~8 全拍一遍,同时回读 `doc.ActiveView.Transform.ArrayData`, 用旋转矩阵第三列判断相机视线方向,实测映射: | vid | 视线方向 | 实际是 | |---:|---|---| | 0 | (−1, 0, 0) | 从 +x 看 | | 1 | (0, 0, −1) | **俯视** | | 2 | (0, 0, +1) | 仰视 | | 3 | (+1, 0, 0) | 从 −x 看 | | 4 | (−1, 0, 0) | 从 +x 看 | | 5 | (0, −1, 0) | 北面 | | **6** | **(0, +1, 0)** | **南面** ← 要的就是它 | | 7 | (−0.577, −0.577, −0.577) | 等轴测 | ```python mv = prop(doc, 'ActiveView') arr = list(prop(D(mv), 'Transform').ArrayData) dirv = (arr[2], arr[5], arr[8]) # 相机视线在模型坐标里的指向 ``` ⇒ **要哪个面,就按视线方向反查 id**,不要按名字猜。 (这条和"基准面轴映射"是同一类:SolidWorks 的方位命名在 API 层不可望文生义。) ### ⛔ 在**面**上建的草图,坐标同样要按映射给 —— 不是"接受模型坐标" 托盘柱顶孔那次 `CreateCircleByRadius(M(hx), M(hy), M(POST_H), r)` 成功了, 我据此以为「面上草图接受模型坐标」。**这是巧合**:柱顶面是 **z 常量面**, 它的草图 uv 恰好与模型 xy 一致,第三参给多少都无所谓。 在 **y 常量的端面**上实测(60×20×40 方块的 y=20 面,画草图圆 `(10, 30) r=3`): ``` sketch-local centre = (10.000, 30.000) boss cylinder axis = (10.000, 25.000, −30.000) ← 模型坐标 ``` ⇒ **y 端面草图:u → 模型 x、v → 模型 −z、法线 → +y**(与上视基准面同一套)。 传 `(x, y_face, z)` 会把 `y_face` 当成 v —— 托盘凸台因此被甩到 **z = −104**, 而且那两次异常拉伸**把已有的 8 个孔全吞掉了**(圆柱面直方图从 `{1.7:4, 2.1:4, 4.0:4}` 变成 `{4.0:1}`),整个零件报废。 ⇒ **固定写法**:面上草图一律 `CreateCircleByRadius(M(u), M(v), 0.0, r)`,第三参恒 0, u/v 按该面的实测映射换算;**不要**把模型坐标直接塞进去。 ⚠ 幸亏"分孔径计数"判据当场拦下并 `exit`,v11 没落盘、磁盘上的可用版本没被覆盖 —— 这也是"改几何就换版本号"那条纪律的价值。 ### ⛔ `AddComponent4` 返回 None 的**第三种**成因:资源耗尽(文档数 / 提交量) §15 记过"目标文档必须打开",§34 记过"文件其实没落盘"。本轮遇到第三种: 南板 v3 与顶盖 v3 **明明开着**(`GetPathName` 路径都对)、文件也在盘上(101 / 116 KB), `AddComponent4` 照样返回 `None`;而同一次调用里、同样开着的北板 v2 **插入成功**。 当时状态:**文档数 604**、提交量 **87%**(604 个里绝大多数是废弃版本零件 + 两轮 STEP 导入的临时子件)。与 §34「提交量耗尽 ⇒ SolidWorks 静默放弃操作」同源。 ⇒ **排查顺序**(三种成因逐一排除,别一上来就重启): 1. `os.path.exists` + 文件大小 → 排除"没落盘"; 2. 遍历 `GetDocuments` 看 `GetPathName` → 排除"没打开"; 3. `GetDocumentCount` + 提交量 → 若文档数上百 / 提交量 >85%,**点名关废弃文档**再重试。 ⚠ 清理只能**点名关**(`CloseDoc(标题)`),绝不能 `CloseAllDocuments`: 后者会清空 `%TEMP%\swx*\IC~~\`,废掉总装里底板/刀片/风扇的引用(§19)。 判别标准:`GetPathName` 里含 `\IC~~\` 的一律不动。 ### ⛔ 一个纯 Python 自伤写法:`raise X if cond else None` 想"条件性跳过"时写了 `raise SystemExit(0) if False else None` —— Python 先求三元表达式得到 `None`,再对它执行 `raise`, 于是 `TypeError: exceptions must derive from BaseException` 把脚本炸在半路, 端板 v9 因此没落盘。要跳过就老实写 `if/else`,**别在 `raise` 后面接表达式**。 ### 建模脚本的固定动作:每一步都 `check(期望包围盒)` 本轮就是靠「拉完一步立刻回读包围盒并与期望值比对」把躺错轴当场抓出来的, 而不是等到装进总装才发现。⇒ **每个特征后面跟一个包围盒断言**,不符就立刻停手, 别在错的几何上继续堆特征。 ### 顺带量出来的设计结论 刀片堆叠迎风截面 **y 87.6 × z 67.0 = 5869 mm²**,一只 60×60 风扇正面只有 3600 ⇒ **覆盖 61%**; 按堆叠中心摆时**通道 4(槽4–5,中心 y=81.83)落在风扇正面之外**。 风扇与刀片/底板**零干涉**(fast 口径 360 处全是刀片簧片蹭的基线值)。 ## 36. ✅✅ 面上草图 + `FeatureCut4` **能用** —— 以前的失败主因是**选错了面**(2026-09-13) ### ⛔⛔ `PlaneParams` 是**曲面**法线,不是面的朝外法线 端板 v11 第一次跑,按「`PlaneParams` 法线 +z、位于 z=72」找柱顶面,**三个目标面一个都没找到** (`FeatureCut4` 根本没被调用 —— 别把这种失败记成切除失败)。把零件 14 个平面全列出来逐一核: | 面 | `PlaneParams` 法线 | `FaceInSurfaceSense` | 真实朝外 | |---|---|---|---| | 顶面 z=72 | (0,0,**−1**) | True | +z | | 底面 z=−10 | (0,0,+1) | True | −z | | 外板面 x=0 | (+1,0,0) | True | −x | | 柱端面 x=12.4 | (+1,0,0) | False | +x | | 北侧面 y=104.345 | (0,**−1**,0) | True | +y | ⇒ **本机实测:`sense == True ⇒ 朝外 = −PlaneParams`;`sense == False ⇒ 朝外 = +PlaneParams`**(14/14 无例外)。 ```python n = list(prop(s, 'PlaneParams'))[axis] if prop(fc, 'FaceInSurfaceSense'): n = -n # ← 不加这一行,「按法线选面」约一半概率选反 ``` §34 那条「按法线 +z 选脚顶面,结果选中了立面」、以及「`FeatureCut4` 连续 7 次返回 None」, **高度疑似都是这个**:面选反了,草图建在错的面上,圆落在材料外,切除无物可切 ⇒ None。 ### ✅ 修正选面之后的战绩:6 刀 5 成,唯一失败是草图坐标映射写错 | 面(朝外) | 孔 | 结果 | |---|---|---| | 柱顶 z=72(+z)| 2 × Ø4.2 深 6 | ✅ | | 北侧 y=104.345(+y)| 2 × Ø4.2 深 6 | ✅ | | 南侧 y=−1.655(**−y**),按 u→x、v→−z | 2 × Ø4.2 | ⛔ None(映射错,圆在材料外)| | 南侧(−y),按 **u→x、v→+z** | 2 × Ø4.2 | ✅ | | 脚顶 z=2.4(+z)| 2 × Ø3.4 切穿 | ✅ | 每刀之后都**回读孔的轴线位置**核对(不只数直方图),全部落在设计坐标上,已有孔一个没少。 ### ⛔ y 常量面的草图映射**随朝外方向变** | 面朝外 | u → | v → | |---|---|---| | **+y** | +x | **−z**(§35 那次测的就是这种)| | **−y** | +x | **+z** | ⇒ §35 的映射表只对 +y 面成立。**是 v 轴翻,不是 u 轴翻**(我先猜 u 翻,候选法当场证伪)。 写法:候选映射逐个试,每刀后回读「目标位置有没有出现孔」;错误映射只会把圆画到材料外、返回 None, 不会在错位置切出孔 —— 但仍要守住「已有孔数不能减少」这条。 ### ⭐ 「孔打出来了」≠「螺丝拧得进去」:装配内同轴核对 直方图只证明有几个孔、回读孔位只证明零件内对。**真正的判据是装配坐标下两件的孔轴线对齐**: 把组件的 `Transform2`(列主序 R + T)作用到 `CylinderParams` 的轴点/轴向,逐个配对。 ⚠ 配对要「横向对齐 **且** 轴向最近」:第一版只比横向偏差,南北板过孔的 (x,z) 完全相同, 北侧 4 孔被全部配到南板 —— 报 12/12 通过,**其实没验北板**。脚本:`.claude/r2/shell/check_coaxial.py`。 ### §36 续:x 常量面的草图映射 + 「先打孔、后封口」得盲孔(2026-09-13,两件式顶盖) **x 常量墙面上草图映射**(Ø60 风口 + 4 风扇孔回读位置核过,本地 z 为零件坐标): | 面朝外 | u → | v → | |---|---|---| | **−x** | **+z** | +y | | **+x** | **−z** | +y | 候选法照旧:8 种(交换 × 两符号),目标都在 y>0、z<0,错误映射全部落到墙外、只返回 None,零误切。 **✅ 不用切除特征做盲孔**:想要一根从底面进、顶面看不见的长孔(顶盖角柱的螺母孔), 1. 先拉角柱,孔**画进同一张草图**(此时是通孔); 2. 最后拉顶板(Merge),顶板的实体区域覆盖了孔的上端 ⇒ **孔被封成盲孔**。 回读:4 个 R2.1 圆柱面 z 都是 −84.4~−2.4(上端正好停在顶板下表面)。 同理,底盖 PCB 立柱的螺母孔画进立柱草图、平板草图里不画 ⇒ 盲孔深 = 立柱高。 ⇒ 能靠「特征顺序 + Merge 覆盖」解决的盲孔,就别去碰面上草图切除。 **⚠ 仰视截图看不出贯通孔**:底盖四角过孔后面正对着顶盖角柱底面,着色同灰,孔完全隐形。 孔的存在只能靠几何核对(同轴配对)证明,这类位置别拿截图当判据。 ### §36 续二:同一基准面「两个方向各拉一层」+ 面积判据 + 侧视截图会转 90°(2026-09-13,滤网框) - **不用偏移基准面也能做双层件**:前视基准面画一张草图向 **+z** 拉(贴墙层 0~2),再画一张向 **−z** 拉(外层 −1~0)。 两层的内部开口可以完全不同(窗口 vs 格栅)。偏移面 / `StartOffset` 在本机不可靠,这招绕开了。 - **矩形开口多的时候,用面积当判据**:「朝外 +z、位于 z=2 的平面总面积 = 外框面积 − 开口面积」。 滤网框贴墙面 2456.0、外表面 3051.0 与设计值精确相等 ⇒ 1 个窗口 + 36 个格孔全部开出来了。比数面稳(开口会把面拆碎)。 - **`ShowNamedView2(…, 3)` 出来的侧视图可能整张转 90°**:`tf_west` 竖向是 y、横向是 z。 读截图先用**已知尺寸的宽高比**定方向(110.8/84.4 = 1.31 vs 图上 1.28),再去认特征 —— 否则凸起会被认成错位。 - 文本派生脚本时,锚点替换要**断言出现次数**:本次锚点意外出现 2 次,断言当场拦下,否则会漏改一处。 ## 37. ⛔⛔ 选中了面 ≠ 草图建在这个面上:进草图后读 `ModelToSketchTransform` 验平面(2026-09-13) **事故**:短机版顶盖,东端墙那一刀 `Select4` 返回 True、选中数 1、`GetSelectedObject6` 回读的面 box 也正是东墙 (x=115.9),可 `InsertSketch` 之后草图**建在了顶板 z=0 上** —— Ø60 风口 + 4 个风扇孔从顶板往下切穿, 位置恰好是「东墙草图坐标被当成顶板坐标」。靠「切出了东西但不在目标位置」那道闸才没存盘。 同一段代码上一版(v2)东墙一次成功;本次唯一差别是**前面西墙先有两刀候选映射失败、留下了空草图** ⇒ 疑似残留把状态带偏,**未坐实**。 **✅ 根治:进草图后立刻读草图坐标系**,不对就退出重进: ```python sk = prop(sm, 'ActiveSketch') arr = list(prop(D(sk), 'ModelToSketchTransform').ArrayData) Mx = [[arr[3*c + r] for c in range(3)] for r in range(3)] # ⚠ 列主序 t = arr[9:12] # 模型点 p(米)→ 草图坐标 s = Mx·p + t;Mx 第 3 行 = 草图法线;s[2]==0 ⇔ 点在草图平面上 ``` 实测(同一零件): | 面 | `Mx` 行 0(u)| 行 1(v)| 行 2(法线)| t | |---|---|---|---|---| | 东墙 x=115.9(朝外 +x)| −z | +y | +x | (0, 0, −0.116) | | 西墙 x=−18.4(朝外 −x)| +z | +y | −x | (0, 0, −0.018) | | 顶板 z=0 | +x | +y | +z | 0 | ⇒ 与 §36 用候选法试出来的映射**完全一致**,而且是直接读出来的。 ⇒ **以后面上草图一律**:选面 → `InsertSketch` → 读变换 → 检查「法线轴 = 目标轴」且「墙面上一点的草图 z ≈ 0」 → 用 `to_sketch()` 把模型坐标换成草图坐标再画。**候选映射法作废**(它会留下失败的空草图,恰恰可能是事故诱因)。 改用这套之后:西/东/南/北四刀**全部第 1 次就进对平面、切对位置**。脚本 `.claude/r2/shell/patch_v3_cut.py`。 ### §37 续:面上草图画圆角矩形 —— `CreateArc` 可用 + 面积判据精确到 0.01 mm²(2026-09-13,Type-C 开口) - `ISketchManager.CreateArc(XC, YC, Zc, Xp1, Yp1, Zp1, Xp2, Yp2, Zp2, Direction)`,**10 参**,坐标用草图坐标(米)。 `Direction` 按**草图坐标系**里 (p1−c)×(p2−c) 的符号取:正 ⇒ `1`(逆时针 90° 短弧),负 ⇒ `-1`。 因为墙面草图的 u/v 可能是镜像的(§36/§37 表),**必须在变换后的草图坐标里算叉积**,别在模型坐标里算。 4 直线 + 4 圆弧 ×2 = 16 段,一次闭合、一刀切出,4 个 R0.7 圆角 + 4 条直边逐个回读全部在位。 - **面积判据**:墙外表面面积 = 无开口面积 − Σ开口面积(圆角矩形 = W·H − (4−π)·R²), 实测 10785.99 与理论值**完全相等**;北墙去掉窗口后 10828.52 与无开口理论值完全相等 ⇒「窗口真的没了」也能证明。 - **量连接器口**:嵌套在 STEP 子装配里的零件,`GetComponents(False)` 找到带实体的那一层(名字不带位号的那个), `GetBodies2(0)` 拿本地实体 + `Transform2` 换算。⚠ 遍历全部 1098 个子件三遍会超过 120 s 工具超时,查一次就缓存组件对象。 ## 38. 外观件的三个判据 + 视图 id 不可信(2026-09-13,外壳美化) 1. **格栅 / 多槽开口:切前切后读同一墙面的面积差**,与「Σ槽面积 + 同刀的孔面积」比,实测 2379.94 = 2379.94。 圆角、墙面轮廓被别的特征改过都不影响(差值只含这一刀),比「理论整面面积」稳得多。 2. **槽方向:读槽端半圆柱的包围盒**。竖槽的半圆柱 y 跨槽宽、z 跨半径;横槽反过来。 ⚠ 等轴测截图里竖槽会**投影成斜线**,看起来像横槽 —— 别拿截图判方向。 3. **凹刻符号有没有镜像:`IFace2.GetClosestPointOn(x,y,z)`**。在符号里挑一个 「只有方向正确时才在面内」的点(例如闪电顶部偏右),离面距离 = 0 ⇒ 在面内;再测镜像位置应 ≠ 0。 面积判据对镜像**没有分辨力**(镜像后面积完全一样)。 4. **`ShowNamedView2` 的 id ↔ 方向每次会话都可能变**:§35 那张表这次不成立(本次 0=俯视、3=视线 +x、6=视线 +y、2=仰视)。 ⇒ 一律把 0~8 全切一遍读 `ActiveView.Transform.ArrayData` 的 (a[2], a[5], a[8]) 再挑。脚本 `render_v7.py`。 5. 顶面外轮廓倒圆角:**在加导轨/凸台之前**做,此时顶面外轮廓边 = 4 直边 + 4 段竖边圆角弧, 8 条一起 `Select4(append)` 后一次 `FeatureFillet3` 成功;加了外凸件再做,包围盒判据会漏边。 6. 半圆弧的 `CreateArc` 方向:叉积 (p1−c)×(p2−c) = 0 分不出方向 ⇒ 改用「弧中点」m:(p1−c)×(m−c) > 0 ⇒ `1`。 ## 39. ⛔ 干涉少了一整片刀片 = 有人在 GUI 里隐藏了零件,不是 COM 丢结果(2026-09-13) 复验报 **288**(基线 360),少的正好是一片刀片的 72 处,清单里没有外壳/风扇。 我先按 §28/§34 当成「提交量 94% ⇒ 静默丢结果」,点名关掉 25 个旧文档、提交量降到 75% 后重跑 —— **仍是 288**,判断错了。 逐片查子件状态:中间那片刀片的板体 `Board~MDFG.step-1` **`Visible = 0`**(用户在界面里看模型内部时隐藏的), 而复验一直设着 `idm.IgnoreHiddenBodies = True` ⇒ 这片刀片被排除在外。 `IgnoreHiddenBodies = False` 重跑 ⇒ **360 = 5 × 72** ✅。 ⇒ **复验脚本默认 `IgnoreHiddenBodies = False`**,不要去改用户的显示状态。 ⇒ 干涉数与基线不符时的排查顺序:①按件对统计(`o.Components` 取名分组)看缺的是谁 → ②查那件及子件的 `Visible / GetSuppression2 / IsHidden(True)` → ③再考虑提交量。别一上来就归咎于资源。 ### 两个脚本侧小坑 - **重定向到文件时 `TextIOWrapper(..., write_through=True)` 仍然整块缓冲**,长跑脚本日志要到结束才出现; 想看进度用 `python -u`(本次 v9 用 `-u` 后才能中途看到格栅那几行)。 - **bash heredoc 里的 Python 原始字符串以 `\'` 结尾会被吃成 `\'`**(`r'...tmp\'` → 语法错误)。 含反斜杠路径的补丁脚本一律用 Write 落成 .py(与 §34「中文路径别用 heredoc」同一族)。 ### §39 续:按视线挑视图也会拍错(2026-09-13,v10) `render_v10.py` 里「视线 (0,0,+1) ⇒ 仰视」挑出的 vid 0,存出来的却是南面图(与 `v10_south` 像素一致)。 疑似 `ShowNamedView2` 之后立刻读 `ActiveView.Transform` 读到的是切换前的视角。 ⇒ 读视线前先 `doc.GraphicsRedraw2` / 再调一次 `ViewZoomtofit2`,并**截完图再读一次视线**核对; 对不上就丢弃该图。**几何关系一律以回读数据为准,截图只作人眼辅助。** ## 40. ⛔ 导出 STL 前必须清空选择:有选中的面/边时只导出选中部分(2026-09-13) `part.Extension.SaveAs3('x.stl', …)` 返回 err=0、文件也在,但**顶盖只导出了 252 个三角形**(包围盒 = 最后一步选中的 Type-C 标识凹槽),**底盖只有 36 个**(= add_fillets 最后选中的那条倒角边)。SolidWorks 把「当前选中的实体」当成导出范围。 ⇒ 导出前 `part.ClearSelection2(True)`,并打印 `GetSelectedObjectCount2(-1)` 确认为 0。 STL 导出设置(`swconst.tlb` 读出,别猜):Toggle 69 二进制 / 70 保存后信息框(**关**,否则模态框卡 COM)/ 71 不平移到正坐标 / 191 预览(**关**);Integer 78 精度(3=自定义)/ 211 单位(`swLengthUnit_e.swMM` = 0); Double 2 偏差(米)/ 3 角度公差(弧度)。改前备份、`finally` 里恢复。 判据(`export_stl_v10.py`):numpy-stl 读回 ① 包围盒 = 模型包围盒(顺带验单位)② 每条边恰被 2 个三角形共用(封闭) ③ 体积与 `Extension.CreateMassProperty.Volume` 差 < 0.5%(实测 0.002%)。这次正是 ①②③ 同时报警才发现「只导了一小块」。