skill:solidworks-assembly 实测笔记与脚本、easyeda-sch-geometry 补充

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:42 +08:00
共同撰写人 Claude Opus 5.5
父节点 22a255acd7
当前提交 cd01640c40
共修改 8 个文件,包含 2283 行新增和 1 行删除
@@ -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<PID>\ 里的临时件全丢、底板零部件断引用。
# ⇒ 这里无论如何都要把命令关掉再把异常抛出去。
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
@@ -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()
@@ -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' ⇒ 腔底可能不是平面,或这个模型确实没建腔。')
@@ -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' ⇒ **模型建了腔**,几何冲突是真的,干涉结论可信,可以继续追。')
@@ -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))
@@ -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')