文件
phonerail/.claude/skills/solidworks-assembly/scripts/place_and_probe.py
T

336 行
16 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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