From cd01640c40c2fa4230126478f718dfeb50ccbf86 Mon Sep 17 00:00:00 2001 From: zhou yang Date: Sun, 4 Oct 2026 19:24:42 +0800 Subject: [PATCH] =?UTF-8?q?skill=EF=BC=9Asolidworks-assembly=20=E5=AE=9E?= =?UTF-8?q?=E6=B5=8B=E7=AC=94=E8=AE=B0=E4=B8=8E=E8=84=9A=E6=9C=AC=E3=80=81?= =?UTF-8?q?easyeda-sch-geometry=20=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .claude/skills/easyeda-sch-geometry/SKILL.md | 12 + .../solidworks-assembly/NOTES-probed.md | 1162 ++++++++++++++++- .../scripts/place_and_probe.py | 335 +++++ .../scripts/rebuild_assembly.py | 153 +++ .../scripts/slot_cavity_measure.py | 145 ++ .../scripts/slot_cavity_test.py | 152 +++ .../scripts/slot_depth_measure.py | 173 +++ .../scripts/slot_opening_measure.py | 152 +++ 8 files changed, 2283 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/solidworks-assembly/scripts/place_and_probe.py create mode 100644 .claude/skills/solidworks-assembly/scripts/rebuild_assembly.py create mode 100644 .claude/skills/solidworks-assembly/scripts/slot_cavity_measure.py create mode 100644 .claude/skills/solidworks-assembly/scripts/slot_cavity_test.py create mode 100644 .claude/skills/solidworks-assembly/scripts/slot_depth_measure.py create mode 100644 .claude/skills/solidworks-assembly/scripts/slot_opening_measure.py diff --git a/.claude/skills/easyeda-sch-geometry/SKILL.md b/.claude/skills/easyeda-sch-geometry/SKILL.md index 25b3558..de99ef8 100644 --- a/.claude/skills/easyeda-sch-geometry/SKILL.md +++ b/.claude/skills/easyeda-sch-geometry/SKILL.md @@ -954,6 +954,14 @@ for it in out: (`prio[k] < prio[k+1]`),不能断言「第 k 块的优先级 == k」—— `pcb2_pour.py` 第一版就是后者,白报了一次「优先级和计划对不上」。 +6. ⚠️⚠️ **块数一多,连「相对次序 = 创建顺序」都不成立**(2026-09-10,R2 底板 83 块、分 7 批 `create()`)。 + 读回的 L1 优先级:12V 指头 13~19 → 3~9、GND 底填 12~0 → 10~22、12V 指头 12~5 → 23~30、12V 主块 → 31~34, + 而创建顺序是 12V 全部在前、GND 在后。结果整板那块 GND(18)先铺、把 L1 12V 主块和 J37/J38 指头、 + L4 VIN_SW 主块(23)的地盘占完 —— **83 块只填出 61 块,缺的正是大电流铜**,DRC 和 `board_check` 都不报 + (后者按计划铜面判,读不到填充)。 + ✅ **别靠优先级做分割**:GND 底填在几何上**挖掉同层功率铜面**(`pour_base.plan()` 的 `pwr_cut`), + 两边不重叠,谁先铺都一样。核对铺没铺上:数 `pcb_PrimitivePoured` 对应的铜面名,功率铜面一块都不许空。 + ## 9j-2. 铺铜的焊盘连接方式:`connectMode` 藏在 DRC 规则里,`0` = 十字花 / `1` = 全连接(2026-09-05)⚠️⚠️ **`PCB_PrimitivePour` 上没有这个属性** —— `create()` 的 9 个参数里没有, @@ -1538,6 +1546,10 @@ PCB 网表里那四颗的地端是 **`$16N236` 而不是 `GND`**; `pcb_PrimitiveComponent` 上**有 `setState_UniqueId()`**(`setState_Footprint` 没有, 所以换封装只能删了重建,见 §9z 一节的 PCB 版)。 +✅ 2026-09-11 补:**只改封装内容(不换封装)不用删重建** —— PCB 实例引用的是**工程库副本** +(`getState_Footprint()` 读出 `{uuid, libraryUuid}`,libraryUuid 是工程库,不是个人库)。 +`lib_Footprint.openInEditor(副本uuid, 工程库uuid)` 里改完存盘,**PCB 页 reload 之后实例就跟着变**,焊盘网名按脚号保留 +(刀片 J1 缺口 1.9 → 1.6,`r2/fp_edge.py --notch`,读回 64/64 网名不变)。只改个人库原件是不会传过去的。 ⚠️ **必须两阶段写**:这是一个**排列**(J27 的 ID 要给 J26), 直接按目标值挨个写会在中途出现两个元件同 ID。 diff --git a/.claude/skills/solidworks-assembly/NOTES-probed.md b/.claude/skills/solidworks-assembly/NOTES-probed.md index bfdb7d7..fe7e1cb 100644 --- a/.claude/skills/solidworks-assembly/NOTES-probed.md +++ b/.claude/skills/solidworks-assembly/NOTES-probed.md @@ -203,6 +203,17 @@ 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 二次更正)**: @@ -220,7 +231,14 @@ n = asm.ToolsCheckInterference2(0, EMPTY, False, pcomp, pface) (刀片板体包围盒 `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]`。 +`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 | |---|---|---| @@ -268,8 +286,26 @@ c = asm.AddComponent4(path, '', 0.0, 0.0, 0.0) # (CompName, ConfigName, X, Y 实测结果:两块板都插进同一个总装,`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 @@ -371,3 +407,1127 @@ mu = wc.Dispatch(raw) # → 可用的 IMathUt 卡点仍是 §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%)。这次正是 ①②③ 同时报警才发现「只导了一小块」。 diff --git a/.claude/skills/solidworks-assembly/scripts/place_and_probe.py b/.claude/skills/solidworks-assembly/scripts/place_and_probe.py new file mode 100644 index 0000000..7a561f1 --- /dev/null +++ b/.claude/skills/solidworks-assembly/scripts/place_and_probe.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +u"""把刀片竖插到底板槽位 + 干涉扫描,量出金手指插进插槽塑料体的深度 d。 + +用法: + python place_and_probe.py validate [槽号0..4] # 摆位到槽口上方,验证旋转约定 + python place_and_probe.py sweep [槽号0..4] # 扫 dz 找首次接触,再扫 y 找腔中心 + python place_and_probe.py restore # 还原刀片到脚本首次运行前的位置 + +⛔ 绝不调用 CloseAllDocuments —— 底板零部件引用 %TEMP%\\swx*\\IC~~\\,一关就断链。 + +━━ 实测约定(都踩过,见 NOTES-probed.md)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. `CreateTransform` 只能走 dispid + `VARIANT(VT_ARRAY|VT_R8)`。 + 传 list / 普通 VARIANT → "服务器出现意外情况"; + 传 tuple → **不报错但静默返回单位阵**。⇒ 必须回读校验。 + +2. ⛔⛔ `ToolsCheckInterference2` **是死的**:在"刀片板体直接穿过底板板体" + 这种必然干涉的姿态下仍返回 `None` / PComp 0 条。它不抛异常、返回值看着像 + 正常答案 —— 拿它做判据会得出"全程无干涉"的假结论。**改用 + `IInterferenceDetectionMgr`**(还附带干涉体积)。 + +3. `GetInterferenceCount` / `GetInterferences` 在 tlb 里标 invkind=1(方法), + 但动态派发下必须**当属性取**(不加括号),同 NOTES §9。 + +4. 旋转矩阵 `ArrayData[0:9]` 是**列主序**(NOTES §14 原来写的"行主序"是错的)—— + 本脚本用 `validate` 在装配体上现场判定,不写死。 + +5. 底板自身就有几十处预存干涉(器件 3D 模型压进板体等),所以**只统计 + 刀片子树 ↔ 底板子树的跨对**,否则信号被淹没。 +""" +import io +import json +import os +import sys +import time + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +import pythoncom +import win32com.client as wc + +HERE = os.path.dirname(os.path.abspath(__file__)) +BACKUP = os.path.join(HERE, '_blade_xform_backup.json') + +# ── 几何常量(出处:.claude/r2/pcb_base.py / pcb_blade.py,板局部坐标 mm)────── +SLOT_CX = 49.0 # 底板上槽心 x +SLOT_YS = [8.36, 28.68, 49.0, 69.32, 89.64] +J1_X = 45.0 # 刀片金手指阵列中心 x(= BW/2,刀片 90 宽) +BLADE_T = 1.67 # STEP 实测板厚 +DX = SLOT_CX - J1_X # = 4.00 +STEP_H = 12.0 # 刀片台阶高(舌头两侧肩部底边在刀片局部 y=12.0) + +# 底板 3D 实测(底板局部坐标):板体 z 0.00~1.67; +# 插槽 PCIE-64P11L 塑料体 z 0.77~12.33,体高 11.56(**不是设计文档写的 ~9**)。 +SLOT_TOP = 12.33 # 塑料体顶面 z ⇒ 插入深度 d = SLOT_TOP − dz +SLOT_MODEL_DY = -0.45 # 插槽 3D 模型 y 中心相对焊盘中心的偏移(五槽一致) + +ROT_A = [1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0] +ROT_B = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0] + + +def connect(tries=12): + u"""⚠ SolidWorks 忙(长时间干涉计算 / 重建)时会**暂时从运行对象表摘掉自己**, + `GetActiveObject` 报 `-2147221021 操作无法使用`。这**不是崩溃** —— + 进程还在、文档还开着、临时件引用链也没断,等几秒重试即可。 + 别据此判定 SolidWorks 挂了而去重启或重导 STEP(那才会真把引用链搞断)。""" + for k in range(tries): + try: + return wc.GetActiveObject('SldWorks.Application') + except Exception: + if k == tries - 1: + raise + time.sleep(4) + + +def mk_xform(app, arr16): + u"""arr16 → IMathTransform。**必须回读校验**,见文件头第 1 条。""" + did = app._oleobj_.GetIDsOfNames('GetMathUtility') + mu = wc.Dispatch(app._oleobj_.Invoke(did, 0, pythoncom.DISPATCH_METHOD, 1)) + did2 = mu._oleobj_.GetIDsOfNames('CreateTransform') + v = wc.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, list(arr16)) + xf = wc.Dispatch(mu._oleobj_.Invoke(did2, 0, pythoncom.DISPATCH_METHOD, 1, v)) + got = list(xf.ArrayData) + if max(abs(a - b) for a, b in zip(got, arr16)) > 1e-9: + raise RuntimeError(u'CreateTransform 回读不符(多半被静默降成单位阵):%r' % (got,)) + return xf + + +def blade_arr(rot9, slot_i, dz, ycen=None, ddx=0.0): + u"""rot9 3x3 + 平移 → 16 元素数组(平移转米)。 + + ycen = 刀片板厚中面要对到的底板 y(默认焊盘槽心); + ddx = 在标称 DX 之上再沿 x 平移多少(用来试"舌头是不是比腔长")。""" + if ycen is None: + ycen = SLOT_YS[slot_i] + return list(rot9) + [(DX + ddx) / 1000.0, (ycen + BLADE_T / 2.0) / 1000.0, + dz / 1000.0, 1.0, 0.0, 0.0, 0.0] + + +def get(app): + doc = app.ActiveDoc + comps = doc.GetComponents(True) + blade = [c for c in comps if 'blade' in c.Name2.lower()][0] + base = [c for c in comps if c is not blade][0] + return doc, blade, base + + +def box(c): + return [round(v * 1000.0, 3) for v in c.GetBox(False, False)] + + +def place(app, doc, blade, xf): + try: + blade.SetTransformAndSolve2(xf) + except Exception: + blade.Transform2 = xf + doc.EditRebuild3 + return box(blade) + + +def cross_pairs(doc, dump=False, fast=False): + u"""只返回「刀片子树 ↔ 底板子树」的干涉,[(体积mm^3, 名A, 名B), ...],按体积降序。 + + fast=True:`TreatSubAssembliesAsComponents=True`,只按**两个顶层零部件**算 —— + 底板自身那 245 处内部干涉直接不参与计算,单次由 ~41 s 降到秒级, + 且结果天然只剩跨对。代价是拿不到叶子零件名(只知道"刀片撞底板",不知撞的是谁)。 + ⇒ 布尔型的扫描(找窗口边界)用 fast;要指名道姓时用全量。 + 两者必须在**同一个已知干涉的姿态**上对过口径才能混用(main 里会验一次)。 + """ + idm = doc.InterferenceDetectionManager + idm.TreatCoincidenceAsInterference = False + idm.IncludeMultibodyPartInterferences = not fast + idm.TreatSubAssembliesAsComponents = fast + idm.IgnoreHiddenBodies = True + idm.UseTransform = False + out, total = [], 0 + + def members(): + u"""⚠ 连调几十轮后 `idm.GetInterferences` 会突然抛 AttributeError + (动态派发的包装失效)。按 dispid 直调兜底,同 §17 的招数。""" + try: + return idm.GetInterferences + except AttributeError: + did = idm._oleobj_.GetIDsOfNames('GetInterferences') + return idm._oleobj_.Invoke(did, 0, pythoncom.DISPATCH_PROPERTYGET, 1) + + try: + try: + n = idm.GetInterferenceCount + except AttributeError: + did = idm._oleobj_.GetIDsOfNames('GetInterferenceCount') + n = idm._oleobj_.Invoke(did, 0, pythoncom.DISPATCH_PROPERTYGET, 1) + total = n or 0 + if n: + for it in members(): + o = it if hasattr(it, 'Volume') else wc.Dispatch(it) + names = [wc.Dispatch(c).Name2 for c in o.Components] + if dump and len(out) == 0: + print(u' [诊断] 全名样本: %r' % (names,)) + if len(names) != 2: + continue + a, b = names + if ('blade' in a.lower()) != ('blade' in b.lower()): + out.append((o.Volume * 1e9, a.split('/')[-1], b.split('/')[-1])) + finally: + try: idm.Done() + except Exception: pass + out.sort(reverse=True) + return out, total + + +def main(): + mode = sys.argv[1] if len(sys.argv) > 1 else 'validate' + slot_i = int(sys.argv[2]) if len(sys.argv) > 2 else 2 # 默认中间槽 J36 + + app = connect() + doc, blade, base = get(app) + print(u'活动文档: %s' % doc.GetTitle) + print(u'刀片: %s 底板: %s' % (blade.Name2, base.Name2)) + + if not os.path.exists(BACKUP): + json.dump({'name': blade.Name2, 'arr': list(blade.Transform2.ArrayData)}, + open(BACKUP, 'w'), indent=1) + print(u'已备份刀片原始变换 → %s' % BACKUP) + + if mode == 'restore': + arr = json.load(open(BACKUP))['arr'] + print(u'还原 → %s' % place(app, doc, blade, mk_xform(app, arr))) + return + + # ── 验证旋转约定:用一个明显够不着的高度 ──────────────────────────── + good = None + for tag, rot in ((u'ROT_A', ROT_A), (u'ROT_B 转置', ROT_B)): + b = place(app, doc, blade, mk_xform(app, blade_arr(rot, slot_i, 20.0))) + ok = (abs(b[0] - DX) < 0.3 and abs(b[3] - (DX + 90)) < 0.3 + and b[2] > 19.0 and b[5] > 80.0) + print(u' %-12s → box %s %s' % (tag, b, u'✅' if ok else u'❌')) + if ok: + good = rot + break + if good is None: + print(u'⛔ 两种约定都不对,停手'); return + + slot_y = SLOT_YS[slot_i] + + # ── 基线:刀片停在够不着的高度,跨对必须为 0 ───────────────────────── + t0 = time.time() + base_pairs, base_total = cross_pairs(doc, dump=True) + print(u'\n基线 (dz=20,刀片悬空):全装配干涉 %d 处,其中刀片↔底板跨对 %d 处 [%.1fs]' + % (base_total, len(base_pairs), time.time() - t0)) + if base_pairs: + print(u' ⚠ 悬空时就有跨对,说明过滤口径不对,先看这些:') + for v, a, b in base_pairs[:5]: + print(u' %10.3f mm^3 %s <-> %s' % (v, a[:34], b[:34])) + return + if base_total == 0: + print(u' ⛔ 全装配 0 处干涉 —— 底板自身预存干涉应该非 0,检测可能又是假阴性,停手') + return + + if mode == 'validate': + print(u'\n旋转约定:%s;检测基线正常。刀片停在 dz=20(槽口上方)。' + % (u'ROT_A' if good is ROT_A else u'ROT_B 转置')) + return + + def probe(dz, ycen=None, ddx=0.0, fast=False): + place(app, doc, blade, mk_xform(app, blade_arr(good, slot_i, dz, ycen, ddx))) + return cross_pairs(doc, fast=fast)[0] + + # ── 扫 dz ──────────────────────────────────────────────────────────── + print(u'\n槽 %d(焊盘槽心 y=%.2f)dz 扫描' % (slot_i + 1, slot_y)) + print(u' dz = 刀片底边(金手指尖)的 z;塑料体顶面 z=%.2f;' % SLOT_TOP) + print(u' 肩部(台阶 %.1f)碰塑料体顶面应发生在 dz=%.2f;底板板体顶面 z=1.67。' + % (STEP_H, SLOT_TOP - STEP_H)) + first = None + for k in range(24): + dz = round(13.0 - 0.5 * k, 2) + if dz < 1.5: + break + t0 = time.time() + ps = probe(dz) + tot = sum(p[0] for p in ps) + print(u' dz=%6.2f 跨对 %2d 体积 %9.3f mm^3 [%.1fs] %s' + % (dz, len(ps), tot, time.time() - t0, + (u'%s <-> %s' % (ps[0][1][:26], ps[0][2][:26])) if ps else u'')) + if ps: + first = dz + break + if first is None: + print(u'\n⛔ 一路扫到 dz=1.5(刀片快碰到底板板面)都没有跨对干涉') + print(u' ⇒ 插槽塑料体的 3D 模型很可能是**没有建腔的空壳/或腔通到底**,量不出 d。') + return + + lo, hi = first, first + 0.5 + for _ in range(5): + mid = round((lo + hi) / 2.0, 3) + if probe(mid): lo = mid + else: hi = mid + d = SLOT_TOP - hi + print(u'\n首次接触 dz ≈ %.3f(恰好不干涉 %.3f)' % (lo, hi)) + print(u'⇒ **插入深度 d = %.2f − %.3f = %.2f mm**' % (SLOT_TOP, hi, d)) + print(u'⇒ 刀片底边离底板板面 = %.2f mm(设计文档写的「约 11 mm」对不对看这个数)' % (hi - 1.67)) + ps = probe(lo) + print(u' 刚接触时挡住的是:') + for v, a, b in ps[:4]: + print(u' %10.4f mm^3 %s <-> %s' % (v, a[:36], b[:36])) + + # ── 对口径:fast 与全量必须在同一个已知干涉的姿态上一致 ──────────────── + place(app, doc, blade, mk_xform(app, blade_arr(good, slot_i, lo))) + t0 = time.time(); n_full = len(cross_pairs(doc)[0]); t_full = time.time() - t0 + t0 = time.time(); n_fast = len(cross_pairs(doc, fast=True)[0]); t_fast = time.time() - t0 + print(u'\n口径对账 @dz=%.3f:全量 %d 处 [%.1fs] / fast %d 处 [%.1fs]' + % (lo, n_full, t_full, n_fast, t_fast)) + if (n_full > 0) != (n_fast > 0): + print(u' ⛔ 两种口径不一致,后面的扫描不能用 fast,停手'); return + print(u' ✅ 一致,布尔扫描改用 fast(快 %.0f 倍)' % (t_full / max(t_fast, 1e-3))) + + # ── 在腔内扫 y:腔的真实中心在哪(分辨假设 B:模型 y 偏心 0.45)──────── + probe_z = round(hi - 1.0, 3) # ⚠ 插得更深 = dz 更**小**,别写成 + + print(u'\n[假设B] 在 dz=%.3f(比刚接触再低 1.0)处扫 y:' % probe_z) + ys = [round(slot_y - 1.6 + 0.1 * k, 2) for k in range(33)] + free = [y for y in ys if not probe(probe_z, y, fast=True)] + if free: + cen = (free[0] + free[-1]) / 2.0 + print(u' 无干涉窗口 y = %.2f ~ %.2f(宽 %.2f),中心 %.2f,相对焊盘槽心 %+.2f' + % (free[0], free[-1], free[-1] - free[0], cen, cen - slot_y)) + print(u' (包围盒推出的模型偏移是 %+.2f;若两者接近 ⇒ 假设B 成立,是对中问题)' + % SLOT_MODEL_DY) + else: + cen = None + print(u' ⛔ ±1.6 内处处干涉 ⇒ 不是 y 对中的事,假设B 不成立') + + # ── 扫 x:舌头是不是比腔长(分辨假设 A:36.0 偏宽)──────────────────── + print(u'\n[假设A] 在同一深度左右平移刀片,看舌头两端是不是顶在腔的端壁上:') + print(u' (若是"太宽",那么无论怎么平移都插不进;若只是偏心,平移能找到窗口)') + hits = [] + for k in range(17): + ddx = round(-2.0 + 0.25 * k, 2) + n = len(probe(probe_z, cen, ddx=ddx, fast=True)) + hits.append((ddx, n)) + okx = [d for d, n in hits if n == 0] + if okx: + print(u' x 平移 %+.2f ~ %+.2f 时无干涉 ⇒ **腔够长**,是对中/偏移问题' % (okx[0], okx[-1])) + else: + # ⛔⛔ 别在这里下"舌头太宽"的结论 —— 这个测试**没有分辨力**。 + # y/x 扫描都是在首次接触**以下**做的。若插槽模型压根是个没建腔的实心块, + # 那么任何 y、任何 x 都必然干涉,现象与"腔比舌头短"**完全相同**。 + # ⇒ 必须先用「实体体积 / 包围盒体积 + 面数」判定模型有没有腔(见 NOTES §24), + # 确认有腔之后,这里的"全程干涉"才能读作"舌头比腔长"。 + print(u' 全程 %+.2f ~ %+.2f 都干涉。' % (hits[0][0], hits[-1][0])) + print(u' ⚠ 但这**推不出**"舌头 36.0 比腔长" —— 若插槽模型是无腔实心块,' + u'现象一模一样。') + print(u' ⇒ 先判模型有没有腔(体积比 + 面数),再回来读这一段。') + + place(app, doc, blade, mk_xform(app, blade_arr(good, slot_i, hi, cen))) + print(u'\n刀片停在 dz=%.3f, ycen=%s,box=%s' % (hi, cen, box(blade))) + + +if __name__ == '__main__': + try: + main() + except Exception: + # ⛔⛔ 本会话的惨痛教训(NOTES §22):脚本崩在干涉检查中途,会把 SolidWorks + # 留在「干涉检查」这个**活动命令**里。之后它会把自己从运行对象表撤销, + # `GetActiveObject` 一律报 -2147221021,**客户端侧无解、只能重启**, + # 而重启会让 %TEMP%\swx\ 里的临时件全丢、底板零部件断引用。 + # ⇒ 这里无论如何都要把命令关掉再把异常抛出去。 + try: + _a = wc.GetActiveObject('SldWorks.Application') + _d = _a.ActiveDoc + if _d is not None: + _d.InterferenceDetectionManager.Done() + print(u'[兜底] 已关闭干涉检查命令') + except Exception as _e: + print(u'[兜底] 关闭失败(可能已失联): %s' % str(_e)[:90]) + raise diff --git a/.claude/skills/solidworks-assembly/scripts/rebuild_assembly.py b/.claude/skills/solidworks-assembly/scripts/rebuild_assembly.py new file mode 100644 index 0000000..dd45bdb --- /dev/null +++ b/.claude/skills/solidworks-assembly/scripts/rebuild_assembly.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +u"""从两份 STEP 全自动重建「底板 + 刀片」总装,供 place_and_probe.py 接手。 + +用法: + python rebuild_assembly.py # 全自动:起 SolidWorks → 导两份 STEP → 建总装 → 插两块板 + +为什么要有这个脚本:SolidWorks 的运行对象表注册一旦被撤销(见 NOTES §22), +客户端侧无解,只能重启。重启后 `%TEMP%\\swx<旧PID>\\` 里的临时件全没了, +原装配体的底板零部件会丢引用 ⇒ 与其修,不如按 NOTES §15 那条**已验证的链路**重建。 + +━━ 关键约定 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +· 被插的必须是**临时目录里的顶层 SLDASM**(`GetDocuments` 遍历着找), + 不能另存(§11 那两个是废件),也不能猜文件名(真名是 `xxx**.step**.SLDASM`)。 +· 用 `AddComponent4`(5 参),不是 `AddComponent5`(8 参恒返回 None,§15)。 +· ⛔ `AddComponent4` 插进来的件会被**重心归零**(§14)⇒ 板坐标公式全失效。 + 对策:插完**强制把底板零部件的变换设成单位阵**,回读包围盒确认变回 + `0.013~97.987`(= 原点回到板左下角),这样 place_and_probe 的公式可原样用。 +· ⛔ `CreateTransform` 只能走 dispid + `VARIANT(VT_ARRAY|VT_R8)`;传 tuple 会**静默返回单位阵**。 +""" +import io +import os +import sys +import time + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +import pythoncom +import win32com.client as wc + +SRC = r'D:\DCDC\新建文件夹' +BASE_STEP = os.path.join(SRC, '3D_BASE-PCB_2026-09-12.step') +BLADE_STEP = os.path.join(SRC, 'PhoneRail_blade.step') + +IDENT = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] + + +def mk_xform(app, arr16): + did = app._oleobj_.GetIDsOfNames('GetMathUtility') + mu = wc.Dispatch(app._oleobj_.Invoke(did, 0, pythoncom.DISPATCH_METHOD, 1)) + d2 = mu._oleobj_.GetIDsOfNames('CreateTransform') + v = wc.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, list(arr16)) + xf = wc.Dispatch(mu._oleobj_.Invoke(d2, 0, pythoncom.DISPATCH_METHOD, 1, v)) + if max(abs(a - b) for a, b in zip(list(xf.ArrayData), arr16)) > 1e-9: + raise RuntimeError(u'CreateTransform 回读不符(多半被静默降成单位阵)') + return xf + + +def launch(tries=20): + u"""Dispatch 在没有实例时会**起一个新的**——这里正是我们要的。""" + for k in range(tries): + try: + app = wc.Dispatch('SldWorks.Application') + app.Visible = True + _ = app.RevisionNumber + return app + except Exception as e: + if k == tries - 1: + raise + print(u' 启动重试 %d/%d (%s)' % (k + 1, tries, str(e)[:60])) + time.sleep(5) + + +def import_step(app, path, tag): + t0 = time.time() + data = app.GetImportFileData(path) + err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) + doc = app.LoadFile4(path, 'r', data, err) + print(u' %s 导入完成 err=%s 用时 %.1fs 文档数 %s' + % (tag, err.value, time.time() - t0, app.GetDocumentCount)) + return doc + + +def find_top_sldasm(app, keyword): + u"""在打开的文档里找临时目录下的顶层 SLDASM。⛔ 别用 glob 猜名字。""" + best = None + for d in app.GetDocuments: + try: + p, t = d.GetPathName, d.GetType + except Exception: + continue + if not p or t != 2 or not p.upper().endswith('.SLDASM'): + continue + if keyword.lower() in os.path.basename(p).lower(): + best = p + return best + + +def main(): + for f in (BASE_STEP, BLADE_STEP): + if not os.path.exists(f): + print(u'⛔ 源文件不存在: %s' % f); return + + print(u'=== 启动 SolidWorks ===') + app = launch() + print(u' Revision %s 初始文档数 %s' % (app.RevisionNumber, app.GetDocumentCount)) + + print(u'\n=== 导入 STEP(底板 54 MB ≈ 20s,刀片 31 MB ≈ 11s)===') + import_step(app, BASE_STEP, u'底板') + import_step(app, BLADE_STEP, u'刀片') + + base_asm = find_top_sldasm(app, '3D_BASE-PCB') + blade_asm = find_top_sldasm(app, 'PhoneRail_blade') + print(u'\n 底板顶层: %s' % base_asm) + print(u' 刀片顶层: %s' % blade_asm) + if not base_asm or not blade_asm: + print(u'⛔ 没找齐顶层 SLDASM,停手'); return + + print(u'\n=== 新建总装 ===') + tmpl = None + for pref in (9, 10, 8): # swDefaultTemplateAssembly 通常是 9 + try: + v = app.GetUserPreferenceStringValue(pref) + if v and v.lower().endswith('.asmdot'): + tmpl = v; print(u' 模板(pref=%d): %s' % (pref, v)); break + except Exception: + pass + if not tmpl: + print(u'⛔ 取不到装配体模板路径,停手'); return + asm = app.NewDocument(tmpl, 0, 0.0, 0.0) + asm = app.ActiveDoc + print(u' 新建: %s type=%s' % (asm.GetTitle, asm.GetType)) + + print(u'\n=== 插入两块板(AddComponent4,5 参)===') + for tag, p in ((u'底板', base_asm), (u'刀片', blade_asm)): + c = asm.AddComponent4(p, '', 0.0, 0.0, 0.0) + print(u' %s → %s' % (tag, c.Name2 if c else u'⛔ None(换 AddComponent4 也不行才是真问题)')) + asm.EditRebuild3 + + comps = asm.GetComponents(True) + print(u'\n 顶层零部件 %d 个' % len(comps)) + base = [c for c in comps if 'blade' not in c.Name2.lower()][0] + + print(u'\n=== 把底板变换归为单位阵(撤销 AddComponent4 的重心归零,§14)===') + print(u' 归零前 box %s' % [round(v * 1000, 3) for v in base.GetBox(False, False)]) + try: + base.SetTransformAndSolve2(mk_xform(app, IDENT)) + except Exception: + base.Transform2 = mk_xform(app, IDENT) + asm.EditRebuild3 + b = [round(v * 1000, 3) for v in base.GetBox(False, False)] + print(u' 归零后 box %s' % b) + ok = abs(b[0] - 0.013) < 0.2 and abs(b[3] - 97.987) < 0.2 + print(u' %s' % (u'✅ 原点回到板左下角,place_and_probe 的公式可直接用' + if ok else u'⛔ 与预期 0.013~97.987 不符,公式不能直接用,先查')) + + for c in comps: + print(u' %-36s box %s' % (c.Name2[:36], [round(v * 1000, 2) for v in c.GetBox(False, False)])) + print(u'\n完成。接着跑:python place_and_probe.py sweep 2') + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/solidworks-assembly/scripts/slot_cavity_measure.py b/.claude/skills/solidworks-assembly/scripts/slot_cavity_measure.py new file mode 100644 index 0000000..0a99ea7 --- /dev/null +++ b/.claude/skills/solidworks-assembly/scripts/slot_cavity_measure.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +u"""直接量 PCIE 插槽模型的**槽腔**尺寸(不靠碰撞反推)。 + +做法:枚举塑料体的所有平面面,挑法线沿 ±Z 的,按 z 分层列出 x/y 跨度与面积。 + · 顶面 = z 最高的朝上平面 + · 槽腔底 = z 明显低于顶面、x 跨度大、y 跨度窄 的朝上平面 +⇒ 一次读出**腔长**(对比舌头 36.0)与**腔宽**(对比 STEP 板厚 1.67 / 标称 1.6)。 + +⛔ 全程不碰 IInterferenceDetectionMgr —— 重度用它之后会丢 ROT 注册(NOTES §24)。 +""" +import io, re, subprocess, sys, time +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +import pythoncom +import win32com.client as wc + +BASE_STEP = r'D:\DCDC\新建文件夹\3D_BASE-PCB_2026-09-12.step' + + +def prop(o, name): + u"""取 0 参成员。动态派发下多数是属性,但有例外(§17 `GetMathUtility`、 + §25 `GetFaces`)——取回来若是个 **python 方法对象**就再调一次。 + ⛔ 不能用 `callable()` 判断:COM 对象一律 callable(§2),那样会把 + 子对象当函数调,报"找不到成员"。只认 python 侧的方法类型。""" + v = getattr(o, name) + if type(v).__name__ in ('method', 'function', 'builtin_function_or_method'): + return v() + return v + + +def sw_pids(): + out = subprocess.run(['tasklist', '/FI', 'IMAGENAME eq SLDWORKS.EXE', '/FO', 'CSV', '/NH'], + capture_output=True, text=True, encoding='gbk', errors='ignore').stdout + return sorted(re.findall(r'"SLDWORKS\.exe","(\d+)"', out, re.I)) + + +app = None +try: + app = wc.GetActiveObject('SldWorks.Application') + print(u'✅ 复用现有实例(文档数 %s)' % app.GetDocumentCount) +except Exception as e: + print(u'✗ 连不上 (%s) → 自愈' % str(e)[:50]) + if sw_pids(): + subprocess.run(['taskkill', '/F', '/IM', 'SLDWORKS.exe'], capture_output=True) + time.sleep(4) + for k in range(20): + try: + app = wc.Dispatch('SldWorks.Application'); app.Visible = True + _ = app.RevisionNumber; break + except Exception: + if k == 19: raise + time.sleep(5) + +if app.GetDocumentCount == 0: + print(u'导入底板 STEP…') + t0 = time.time() + data = app.GetImportFileData(BASE_STEP) + err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) + app.LoadFile4(BASE_STEP, 'r', data, err) + print(u' err=%s %.1fs 文档数 %s' % (err.value, time.time() - t0, app.GetDocumentCount)) + +part = None +for d in app.GetDocuments: + try: + if d.GetType == 1 and 'PCIE' in d.GetTitle.upper() and '64P11L' in d.GetTitle.upper(): + part = d; break + except Exception: + pass +if part is None: + print(u'⛔ 没找到插槽零件'); sys.exit(1) + +print(u'\n零件: %s' % part.GetTitle) +bb = [v * 1000 for v in part.GetPartBox(True)] +print(u'包围盒 x %.2f~%.2f y %.2f~%.2f z %.2f~%.2f' + % (bb[0], bb[3], bb[1], bb[4], bb[2], bb[5])) + +bodies = part.GetBodies2(0, True) +print(u'实体 %d 个' % len(bodies)) + +rows = [] +for bi, bd in enumerate(bodies): + body = bd if hasattr(bd, '_oleobj_') else wc.Dispatch(bd) + try: + faces = prop(body, 'GetFaces') + except Exception as e: + print(u' 实体%d GetFaces 失败 %s' % (bi, str(e)[:70])); continue + try: + vol = body.GetMassProperties(1.0)[3] * 1e9 + except Exception: + vol = float('nan') + try: + bx = [v * 1000 for v in prop(body, 'GetBodyBox')] + bxs = u'x %.2f~%.2f y %.2f~%.2f z %.2f~%.2f' % (bx[0], bx[3], bx[1], bx[4], bx[2], bx[5]) + except Exception: + bxs = u'?' + print(u' 实体%d: %d 个面, 体积 %.1f mm^3, %s' % (bi, len(faces), vol, bxs)) + for f in faces: + face = f if hasattr(f, '_oleobj_') else wc.Dispatch(f) + try: + surf = prop(face, 'GetSurface') + if not prop(surf, 'IsPlane'): + continue + p = list(prop(surf, 'PlaneParams')) + nz = p[2] + if abs(nz) < 0.99: + continue + fb = [v * 1000 for v in prop(face, 'GetBox')] + rows.append((round(fb[5], 3), nz, prop(face, 'GetArea') * 1e6, + fb[0], fb[3], fb[1], fb[4], bi)) + except Exception: + continue + +print(u'\n=== 法线沿 ±Z 的平面(z 从高到低)===') +print(u' z 朝向 面积mm^2 x 跨度 y 跨度') +rows.sort(reverse=True) +for z, nz, area, x0, x1, y0, y1, bi in rows[:30]: + print(u' %7.3f %+5.1f %9.2f %7.2f~%7.2f(%5.2f) %6.2f~%6.2f(%5.2f) 体%d' + % (z, nz, area, x0, x1, x1 - x0, y0, y1, y1 - y0, bi)) +print(u' …共 %d 个朝 ±Z 的平面' % len(rows)) + +up = [r for r in rows if r[1] > 0] +if not up: + print(u'\n⛔ 一个朝上的平面都没有'); sys.exit() +top = up[0] +print(u'\n塑料体顶面 z=%.3f(面积 %.1f mm^2)' % (top[0], top[2])) +# ⛔⛔ 下面这个"挑面积最大的候选面当腔底"的启发式**不可靠**,实测选出了 +# x -4.11~17.54 这种既不对称也不居中的局部面,并据此打印出"舌头要收窄 14.35" +# 的错误结论。真正的开口尺寸要用 `slot_opening_measure.py`(量顶面内环)。 +# 这里保留只为看分层概况,**不要拿它的数当结论**。 +print(u'\n⚠ 以下"腔底"是启发式猜的,已知会选错面 —— 只作参考,' + u'开口尺寸以 slot_opening_measure.py 为准。') +cav = [r for r in up if r[0] < top[0] - 0.5 and (r[4] - r[3]) > 10 and (r[6] - r[5]) < 4] +if cav: + c = max(cav, key=lambda r: r[2]) + L, W, D = c[4] - c[3], c[6] - c[5], top[0] - c[0] + print(u'\n🎯 槽腔底:z=%.3f' % c[0]) + print(u' 腔深 = %.2f − %.2f = **%.2f mm** ← 这就是一直缺的 d' % (top[0], c[0], D)) + print(u' 腔长 x %.2f~%.2f = **%.2f mm** 舌头 36.00 ⇒ %s' + % (c[3], c[4], L, u'✅ 插得进(余量 %.2f)' % (L - 36.0) if L > 36.0 + else u'⛔ **插不进,舌头要收窄 %.2f 以上**' % (36.0 - L))) + print(u' 腔宽 y %.2f~%.2f = **%.2f mm** STEP 板厚 1.67 ⇒ %s' + % (c[5], c[6], W, u'✅ 进得去' if W > 1.67 + else u'⚠ 模型腔宽 < 1.67,这多半是**模型按 1.6 标称建的**,实物无碍')) +else: + print(u'\n⚠ 没有符合"腔底"特征的朝上平面(z 低于顶面、x 跨度 >10、y 跨度 <4)') + print(u' ⇒ 腔底可能不是平面,或这个模型确实没建腔。') diff --git a/.claude/skills/solidworks-assembly/scripts/slot_cavity_test.py b/.claude/skills/solidworks-assembly/scripts/slot_cavity_test.py new file mode 100644 index 0000000..4721e36 --- /dev/null +++ b/.claude/skills/solidworks-assembly/scripts/slot_cavity_test.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +u"""决定性判据:底板上的 PCIE 插槽 3D 模型,是没建腔的实心块,还是真连接器? + +结论口径: + 实心块 → 实体体积/包围盒 ≈ 1,面数个位~十几 ⇒ 3D **量不出**插入深度 d, + 也判不了"舌头是不是比腔长" + 真连接器 → 体积比明显 <1,面数几百 ⇒ 干涉结论可信,可以继续追 + +为什么独立成一个脚本:它**不需要装配体、不需要刀片、更不碰 IInterferenceDetectionMgr**。 +重度用 IDM 之后 SolidWorks 会丢掉运行对象表注册(NOTES §22/§24), +所以把这个判据做成"一个进程内从头到尾跑完"的形式,不依赖既有连接。 +""" +import io +import os +import re +import subprocess +import sys +import time + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +import pythoncom +import win32com.client as wc + +BASE_STEP = r'D:\DCDC\新建文件夹\3D_BASE-PCB_2026-09-12.step' + + +def sw_pids(): + out = subprocess.run(['tasklist', '/FI', 'IMAGENAME eq SLDWORKS.EXE', '/FO', 'CSV', '/NH'], + capture_output=True, text=True, encoding='gbk', errors='ignore').stdout + return sorted(re.findall(r'"SLDWORKS\.exe","(\d+)"', out, re.I)) + + +def rot_count(): + try: + rot = pythoncom.GetRunningObjectTable() + return sum(1 for _ in rot.EnumRunning()) + except Exception as e: + return u'枚举失败 %s' % str(e)[:40] + + +print(u'=== 诊断 ===') +print(u' SLDWORKS 进程: %s' % sw_pids()) +print(u' 运行对象表条数: %s' % rot_count()) + +app = None +try: + app = wc.GetActiveObject('SldWorks.Application') + print(u' ✅ 现有实例可连,不重启') +except Exception as e: + print(u' ✗ 连不上 (%s)' % str(e)[:50]) + +if app is None: + if sw_pids(): + print(u'\n=== 结束现有实例(已失联,救不回来)===') + subprocess.run(['taskkill', '/F', '/IM', 'SLDWORKS.exe'], + capture_output=True, text=True) + time.sleep(4) + print(u' 剩余: %s' % sw_pids()) + print(u'\n=== 启动新实例 ===') + for k in range(20): + try: + app = wc.Dispatch('SldWorks.Application') + app.Visible = True + _ = app.RevisionNumber + break + except Exception as e: + if k == 19: + raise + print(u' 重试 %d/20 (%s)' % (k + 1, str(e)[:50])) + time.sleep(5) + print(u' Revision %s 文档数 %s' % (app.RevisionNumber, app.GetDocumentCount)) + +if app.GetDocumentCount == 0: + print(u'\n=== 导入底板 STEP(≈50 s)===') + t0 = time.time() + data = app.GetImportFileData(BASE_STEP) + err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) + app.LoadFile4(BASE_STEP, 'r', data, err) + print(u' err=%s 用时 %.1fs 文档数 %s' % (err.value, time.time() - t0, app.GetDocumentCount)) + +# ── 直接在打开的文档里按标题找插槽**零件**,不经装配树 ────────────────── +print(u'\n=== 找插槽零件 ===') +cand = [] +for d in app.GetDocuments: + try: + t, ty = d.GetTitle, d.GetType + except Exception: + continue + if ty == 1 and 'PCIE' in t.upper() and '64P11L' in t.upper(): + cand.append(d) +print(u' 命中 %d 个零件文档' % len(cand)) +if not cand: + print(u' ⛔ 没找到;打开的零件标题样本:') + for d in list(app.GetDocuments)[:12]: + try: + if d.GetType == 1: + print(u' %s' % d.GetTitle[:80]) + except Exception: + pass + sys.exit(1) + +part = cand[0] +print(u' 用: %s' % part.GetTitle) +bb = [v * 1000 for v in part.GetPartBox(True)] +bbv = (bb[3] - bb[0]) * (bb[4] - bb[1]) * (bb[5] - bb[2]) +print(u' 包围盒 %.2f x %.2f x %.2f = %.1f mm^3' + % (bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2], bbv)) + +bodies = None +for args in ((0, True), (-1, True), (0, False)): + try: + bodies = part.GetBodies2(*args) + if bodies: + print(u' GetBodies2%s → %d 个实体' % (str(args), len(bodies))) + break + except Exception as e: + print(u' GetBodies2%s 失败 %s' % (str(args), str(e)[:60])) +if not bodies: + print(u' ⛔ 取不到实体'); sys.exit(1) + +tot, faces = 0.0, 0 +for i, bd in enumerate(bodies): + o = bd if hasattr(bd, 'GetMassProperties') else wc.Dispatch(bd) + try: + tot += o.GetMassProperties(1.0)[3] * 1e9 + except Exception as e: + print(u' 实体%d 体积失败 %s' % (i, str(e)[:60])) + try: + nf = o.GetFaceCount + faces += nf + if i < 8: + print(u' 实体%-2d 面数 %s' % (i, nf)) + except Exception: + pass + +r = tot / bbv if bbv else 0 +print(u'\n 实体体积 %.1f mm^3 / 包围盒 %.1f = **%.3f**' % (tot, bbv, r)) +print(u' 总面数 **%d**%s' % (faces, u' ⚠ 0 是假值(实体不可能 0 面),忽略它' if faces == 0 else u'')) +print() + +# ⛔⛔ 教训:原来这里写的是 `if r > 0.80 or faces < 30`, +# 结果 `GetFaceCount` 返回假值 0 → `faces < 30` 成立 → 一票推翻了 +# 体积比 0.492 给出的相反证据,打印出错误的"实心块"结论。 +# ⇒ 不要把强弱不同的信号用 `or` 并列;面数只在**读到非 0** 时才参与判断。 +faces_ok = faces > 0 +if r > 0.80 or (faces_ok and faces < 30): + print(u' ⇒ **实心块,没有槽腔**。') + print(u' 3D 量不出插入深度 d;"舌头 36.0 比腔长"也**无法由 3D 判定** ——') + print(u' 刀片停在塑料体顶面纯粹是因为模型没开口。只能靠卡尺实测。') +else: + print(u' ⇒ **模型建了腔**,几何冲突是真的,干涉结论可信,可以继续追。') diff --git a/.claude/skills/solidworks-assembly/scripts/slot_depth_measure.py b/.claude/skills/solidworks-assembly/scripts/slot_depth_measure.py new file mode 100644 index 0000000..c47d71c --- /dev/null +++ b/.claude/skills/solidworks-assembly/scripts/slot_depth_measure.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +u"""量 PCIE 插槽的**可插入深度** —— 卡降到哪里会被挡住。 + +限制插入深度的有两个独立约束,必须分开算(很容易只算一个就下结论): + 1. **槽底**:开口区域内最高的朝上平面。 + 2. **防呆键筋**:键筋从顶面往下伸多深。刀片缺口只有 NOTCH_D=7.45 深 —— + 若键筋比缺口深,卡会被**键筋顶住**,先于槽底停下。 + +开口尺寸由 slot_opening_measure.py 量得(顶面 z=10.000,x ±17.540,y −0.160~1.640, +键筋 x −5.390~−4.610)。 + +⛔ 全程不碰 IInterferenceDetectionMgr(NOTES §24)。 +""" +import io, re, subprocess, sys, time +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +import pythoncom +import win32com.client as wc + +BASE_STEP = r'D:\DCDC\新建文件夹\3D_BASE-PCB_2026-09-12.step' + +TOP_Z = 10.000 # 塑料体顶面(零件本地) +OPEN_X0, OPEN_X1 = -17.540, 17.540 # 开口 x +OPEN_Y0, OPEN_Y1 = -0.160, 1.640 # 开口 y +KEY_X0, KEY_X1 = -5.390, -4.610 # 防呆键筋 x +BLADE_T = 1.67 # STEP 刀片板厚 +NOTCH_D = 7.45 # 刀片缺口深度(fp_edge.NOTCH_D) +# 装配坐标换算(由槽3 反推):assembly_z = part_z + 2.33;底板板面 assembly z = 1.67 +Z_OFF, BOARD_TOP = 2.33, 1.67 + + +def prop(o, name): + v = getattr(o, name) + if type(v).__name__ in ('method', 'function', 'builtin_function_or_method'): + return v() + return v + + +def D(x): + return x if hasattr(x, '_oleobj_') else wc.Dispatch(x) + + +def sw_pids(): + out = subprocess.run(['tasklist', '/FI', 'IMAGENAME eq SLDWORKS.EXE', '/FO', 'CSV', '/NH'], + capture_output=True, text=True, encoding='gbk', errors='ignore').stdout + return sorted(re.findall(r'"SLDWORKS\.exe","(\d+)"', out, re.I)) + + +app = None +try: + app = wc.GetActiveObject('SldWorks.Application') + print(u'✅ 复用现有实例(文档数 %s)' % app.GetDocumentCount) +except Exception as e: + print(u'✗ 连不上 (%s) → 自愈' % str(e)[:50]) + if sw_pids(): + subprocess.run(['taskkill', '/F', '/IM', 'SLDWORKS.exe'], capture_output=True) + time.sleep(4) + for k in range(20): + try: + app = wc.Dispatch('SldWorks.Application'); app.Visible = True + _ = app.RevisionNumber; break + except Exception: + if k == 19: raise + time.sleep(5) + +if app.GetDocumentCount == 0: + print(u'导入底板 STEP…') + data = app.GetImportFileData(BASE_STEP) + err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) + t0 = time.time(); app.LoadFile4(BASE_STEP, 'r', data, err) + print(u' err=%s %.1fs' % (err.value, time.time() - t0)) + +part = None +for d in app.GetDocuments: + try: + if d.GetType == 1 and 'PCIE' in d.GetTitle.upper() and '64P11L' in d.GetTitle.upper(): + part = d; break + except Exception: + pass +if part is None: + print(u'⛔ 没找到插槽零件'); sys.exit(1) + +shell, nmax = None, -1 +for bd in part.GetBodies2(0, True): + b = D(bd) + try: + n = len(prop(b, 'GetFaces')) + except Exception: + continue + if n > nmax: + shell, nmax = b, n +print(u'塑料壳:%d 个面\n' % nmax) + +# 刀片板料在通道里实际占据的 y 带(按开口中心对中) +yc = (OPEN_Y0 + OPEN_Y1) / 2.0 +BY0, BY1 = yc - BLADE_T / 2.0, yc + BLADE_T / 2.0 +print(u'开口 y %.3f~%.3f(宽 %.2f);刀片料占 y %.3f~%.3f(厚 %.2f)' + % (OPEN_Y0, OPEN_Y1, OPEN_Y1 - OPEN_Y0, BY0, BY1, BLADE_T)) + +def ovl(a0, a1, b0, b1): + return min(a1, b1) - max(a0, b0) + +up_in_slot, key_faces, all_in = [], [], [] +for f in prop(shell, 'GetFaces'): + fc = D(f) + try: + fb = [v * 1000 for v in prop(fc, 'GetBox')] + except Exception: + continue + if fb[2] > TOP_Z - 1e-6: # 完全在顶面及以上,跳过 + continue + # 与刀片料所占空间在 x/y 上有重叠的面 + if ovl(fb[0], fb[3], OPEN_X0, OPEN_X1) <= 1e-6: + continue + if ovl(fb[1], fb[4], BY0, BY1) <= 1e-6: + continue + nz = None + try: + s = prop(fc, 'GetSurface') + if prop(s, 'IsPlane'): + nz = list(prop(s, 'PlaneParams'))[2] + except Exception: + pass + is_key = ovl(fb[0], fb[3], KEY_X0, KEY_X1) > 1e-6 and (fb[3] - fb[0]) < 2.0 + all_in.append((fb[5], fb, nz, is_key)) + if is_key: + key_faces.append((fb[5], fb)) + elif nz is not None and nz > 0.99: + up_in_slot.append((fb[5], fb)) + +print(u'\n=== 通道内(与刀片料重叠)的面:按 z 顶端排序,前 14 ===') +all_in.sort(reverse=True) +for z, fb, nz, is_key in all_in[:14]: + print(u' z顶 %7.3f z底 %7.3f x %8.3f~%8.3f y %7.3f~%7.3f 法线z=%s %s' + % (fb[5], fb[2], fb[0], fb[3], fb[1], fb[4], + ('%+.2f' % nz) if nz is not None else ' 曲面', u'← 键筋' if is_key else u'')) + +print(u'\n=== 约束 1:槽底(通道内最高的朝上平面,已排除键筋)===') +if up_in_slot: + up_in_slot.sort(reverse=True) + z, fb = up_in_slot[0] + d1 = TOP_Z - z + print(u' 槽底 z = %.3f x %.3f~%.3f y %.3f~%.3f' % (z, fb[0], fb[3], fb[1], fb[4])) + print(u' ⇒ 槽深(顶面到槽底)= %.2f − %.2f = **%.2f mm**' % (TOP_Z, z, d1)) +else: + d1 = None + print(u' ⛔ 通道内没有朝上平面') + +print(u'\n=== 约束 2:防呆键筋 ===') +if key_faces: + kz_top = max(k[0] for k in key_faces) + kz_bot = min(k[1][2] for k in key_faces) + kh = kz_top - kz_bot + print(u' 键筋 z %.3f ~ %.3f,高 **%.2f mm**' % (kz_bot, kz_top, kh)) + print(u' 刀片缺口深 %.2f ⇒ %s' % (NOTCH_D, + u'缺口比键筋深 %.2f,键筋不限制插入' % (NOTCH_D - kh) if NOTCH_D >= kh + else u'**键筋比缺口深 %.2f,卡会被键筋顶住**' % (kh - NOTCH_D))) + d2 = NOTCH_D if kh > NOTCH_D else None +else: + kh, d2 = None, None + print(u' ⛔ 没识别出键筋面') + +print(u'\n=== 结论 ===') +cands = [(u'槽底', d1), (u'防呆键筋', d2)] +cands = [(n, v) for n, v in cands if v is not None] +if cands: + name, d = min(cands, key=lambda t: t[1]) + print(u' 实际可插入深度 d = **%.2f mm**(受「%s」限制)' % (d, name)) + print(u' ⇒ 刀片底边在底板局部 z = %.2f − %.2f = %.2f' + % (TOP_Z + Z_OFF, d, TOP_Z + Z_OFF - d)) + print(u' ⇒ **刀片底边离底板板面 = %.2f mm**(板面 z=%.2f)' + % (TOP_Z + Z_OFF - d - BOARD_TOP, BOARD_TOP)) + print(u' ⇒ 对照设计文档写的「抬高约 11 mm」:上限 10.66(完全不插),实际 %.2f' + % (TOP_Z + Z_OFF - d - BOARD_TOP)) diff --git a/.claude/skills/solidworks-assembly/scripts/slot_opening_measure.py b/.claude/skills/solidworks-assembly/scripts/slot_opening_measure.py new file mode 100644 index 0000000..1641f0f --- /dev/null +++ b/.claude/skills/solidworks-assembly/scripts/slot_opening_measure.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +u"""量 PCIE 插槽**顶面开口**的真实长宽 —— 判断刀片舌头 36.0 插不插得进。 + +为什么不能用碰撞法:插槽模型把簧片触点建成了**刚性**的,两排触点自由间隙只有 +1.22 mm,而 STEP 里刀片板厚 1.67 ⇒ 干涉检查在任何位置都报"插不进", +与舌头宽度无关。所以必须**直接量开口**。 + +两条互相独立的口径(两个都给,不一致就说明哪条的假设不成立): + A. 顶面那个平面的**内环**(inner loop)包围盒 = 开口轮廓 + B. 落在卡槽通道里(|y − 通道中心| 小)的所有面的 x 并集 = 通道长度 + +⛔ 全程不碰 IInterferenceDetectionMgr(NOTES §24)。 +""" +import io, re, subprocess, sys, time +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +import pythoncom +import win32com.client as wc + +BASE_STEP = r'D:\DCDC\新建文件夹\3D_BASE-PCB_2026-09-12.step' + + +def prop(o, name): + u"""0 参成员:多数是属性,但 GetFaces / GetLoops 这类是方法(NOTES §25)。 + ⛔ 不能用 callable() 判断(COM 对象一律 callable,§2),只认 python 方法类型。""" + v = getattr(o, name) + if type(v).__name__ in ('method', 'function', 'builtin_function_or_method'): + return v() + return v + + +def D(x): + return x if hasattr(x, '_oleobj_') else wc.Dispatch(x) + + +def sw_pids(): + out = subprocess.run(['tasklist', '/FI', 'IMAGENAME eq SLDWORKS.EXE', '/FO', 'CSV', '/NH'], + capture_output=True, text=True, encoding='gbk', errors='ignore').stdout + return sorted(re.findall(r'"SLDWORKS\.exe","(\d+)"', out, re.I)) + + +app = None +try: + app = wc.GetActiveObject('SldWorks.Application') + print(u'✅ 复用现有实例(文档数 %s)' % app.GetDocumentCount) +except Exception as e: + print(u'✗ 连不上 (%s) → 自愈' % str(e)[:50]) + if sw_pids(): + subprocess.run(['taskkill', '/F', '/IM', 'SLDWORKS.exe'], capture_output=True) + time.sleep(4) + for k in range(20): + try: + app = wc.Dispatch('SldWorks.Application'); app.Visible = True + _ = app.RevisionNumber; break + except Exception: + if k == 19: raise + time.sleep(5) + +if app.GetDocumentCount == 0: + print(u'导入底板 STEP…') + data = app.GetImportFileData(BASE_STEP) + err = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) + t0 = time.time(); app.LoadFile4(BASE_STEP, 'r', data, err) + print(u' err=%s %.1fs' % (err.value, time.time() - t0)) + +part = None +for d in app.GetDocuments: + try: + if d.GetType == 1 and 'PCIE' in d.GetTitle.upper() and '64P11L' in d.GetTitle.upper(): + part = d; break + except Exception: + pass +if part is None: + print(u'⛔ 没找到插槽零件'); sys.exit(1) +print(u'零件: %s' % part.GetTitle) + +# 取面数最多的那个实体 = 塑料壳 +shell, nmax = None, -1 +for bd in part.GetBodies2(0, True): + b = D(bd) + try: + n = len(prop(b, 'GetFaces')) + except Exception: + continue + if n > nmax: + shell, nmax = b, n +print(u'塑料壳实体:%d 个面' % nmax) +faces = prop(shell, 'GetFaces') + +# ── 找顶面(法线 +Z、z 最高的平面)─────────────────────────────────── +top, topz = None, -1e9 +info = [] +for f in faces: + fc = D(f) + try: + s = prop(fc, 'GetSurface') + if not prop(s, 'IsPlane'): + continue + nz = list(prop(s, 'PlaneParams'))[2] + fb = [v * 1000 for v in prop(fc, 'GetBox')] + info.append((fc, nz, fb)) + if nz > 0.99 and fb[5] > topz: + top, topz = fc, fb[5] + except Exception: + continue +print(u'顶面 z = %.3f' % topz) + +# ── 口径 A:顶面的内环 ─────────────────────────────────────────────── +print(u'\n=== 口径 A:顶面的环(内环 = 开口)===') +try: + loops = prop(top, 'GetLoops') + print(u' 共 %d 个环' % len(loops)) + for li, lp in enumerate(loops): + L = D(lp) + try: + outer = prop(L, 'IsOuter') + except Exception: + outer = '?' + xs, ys = [], [] + try: + for e in prop(L, 'GetEdges'): + E = D(e) + try: + cp = list(prop(E, 'GetCurveParams2')) + pts = [(cp[0], cp[1], cp[2]), (cp[3], cp[4], cp[5])] + except Exception: + pts = [] + for p in pts: + xs.append(p[0] * 1000); ys.append(p[1] * 1000) + except Exception as ex: + print(u' 环%d 取边失败 %s' % (li, str(ex)[:50])); continue + if xs: + print(u' 环%-2d 外环=%-5s x %8.3f~%8.3f (%6.3f) y %7.3f~%7.3f (%5.3f)' + % (li, outer, min(xs), max(xs), max(xs) - min(xs), + min(ys), max(ys), max(ys) - min(ys))) +except Exception as e: + print(u' ⛔ 取环失败: %s' % str(e)[:90]) + +# ── 口径 B:卡槽通道里所有面的 x 并集 ──────────────────────────────── +print(u'\n=== 口径 B:卡槽通道内各面的 x 并集 ===') +YC = 0.74 # 通道中心线(由两排触点 -0.56 / +2.04 推出) +for halfw in (0.7, 0.9, 1.2): + xs = [] + for fc, nz, fb in info: + ymid = (fb[1] + fb[4]) / 2.0 + if abs(ymid - YC) < halfw and fb[5] > 1.5 and fb[2] < topz + 0.01: + xs.append(fb[0]); xs.append(fb[3]) + if xs: + print(u' |y−%.2f|<%.1f ⇒ x %8.3f ~ %8.3f,通道长 **%.2f mm**' + % (YC, halfw, min(xs), max(xs), max(xs) - min(xs))) + +print(u'\n对照:刀片舌头 36.00 mm(TONGUE_X0/X1 = 27.0/63.0,板局部)') +print(u' 塑料体全长 41.00 mm,顶面平面跨度 39.00 mm')