skill:solidworks-assembly 实测笔记与脚本、easyeda-sch-geometry 补充
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -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。
|
||||
|
||||
文件差异内容过多而无法显示
加载差异
@@ -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')
|
||||
在新工单中引用
屏蔽一个用户