# -*- coding: utf-8 -*- u"""从器件的 **3D Model Transform** 直接读出**本体框** —— 不用再截图量。 /c/Python314/python .claude/fp_body.py --scan KF2510 # 按关键词列候选 /c/Python314/python .claude/fp_body.py --scan XT30 --3p # 只看 3 脚 2.54 的 /c/Python314/python .claude/fp_body.py --check # 和 pcb1_place.FP_BODY 对账 ## 为什么能这么读(2026-09-06 验证) §26.6 写过「板边连接器的本体框,除了实测没有别的来源」—— 那一条是**错的**,只是当时没找到路子。`lib_Device.search()` 回来的 `otherProperty['3D Model Transform']` 是一串逗号分隔的数: v[0], v[1] = 3D 本体的 X / Y 尺寸(mil) v[6], v[7] = 本体中心相对**封装原点**的偏移(mil) ⇒ 本体框 = (ox − sx/2, oy − sy/2, ox + sx/2, oy + sy/2)。 **六个已经用放大截图实测过的封装全部对上**(误差 ≤ 0.31 mm,就是截图标定的精度): | 封装 | 3D 算出的 y 向框 | 截图实测 | |---|---|---| | `CONN-TH_KF2510-3A-1`(弯针,§26.6 栽过一次)| −0.31 .. +11.70 | −0.20 .. +11.70 | | `CONN-TH_P5.08_KF128-5.08-2P` | −5.31 .. +5.38 | −5.20 .. +5.20 | | `USB-C_SMD-TYPE-C-31-M-12_1` | −5.05 .. +2.85 | −5.20 .. +3.12 | | `WIRELM-SMD_ESP32-S3-WROOM-1` | −9.10 .. +16.40 | −9.35 .. +16.75 | | `IND-SMD_L7.1-W6.5` | −3.25 .. +3.25 | −3.35 .. +3.35 | | `CAP-TH_BD8.0-P3.50-D0.6-FD` | −4.00 .. +4.00 | −4.10 .. +4.10 | ⚠️ 它给的是 **3D 模型**的框,不是丝印框,也不含公差。当成「本体大致占多少」够用 (这正是摆位要的),要卡到 0.1 mm 的净距还是得看焊盘。**没有 3D 模型的器件读不出来**, 那种还是只能截图。 ## 顺带:立式 / 弯针一眼就分得出 同一个 3P 2.54 端子座: * **立式(直针)**:本体 ≈ 7.6 × 6,偏移 ≈ 0 —— 本体骑在焊盘上 * **弯针(90°)**:本体 ≈ 7.6 × 12,偏移 ≈ ±5.7 —— 本体整个甩到一边 ⚠️ **名字完全不可靠**:`KF2510-3A`(C2940511) 名字像直针,3D 说它是弯针(+5.69); `KF2510-3AW`(C457341) 也是弯针,只是甩向 −Y。要立式的得看数,不能看后缀。 """ import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run # noqa: E402 MIL = 39.3700787401575 def parse(tr): u"""'3D Model Transform' -> (本体 w, h, 框 x0,y0,x1,y1),读不出来返回 None。""" if not tr: return None try: v = [float(x) for x in str(tr).split(',')] except ValueError: return None if len(v) < 8: return None sx, sy = v[0] / MIL, v[1] / MIL ox, oy = v[6] / MIL, v[7] / MIL if sx <= 0 or sy <= 0: return None return (sx, sy, ox - sx / 2, oy - sy / 2, ox + sx / 2, oy + sy / 2) def search(kw, n=40): js = ("const r = await eda.lib_Device.search(%s, undefined, undefined, undefined, %d, 1);" "return (r||[]).map(d => [d.name||'', d.footprintName||'', d.supplierId||''," " (d.otherProperty&&d.otherProperty['3D Model Transform'])||'', d.uuid, d.libraryUuid]);" % (json.dumps(kw), n)) r = run(js, timeout=180) if not r.get('success'): sys.exit(u'搜索失败: ' + json.dumps(r, ensure_ascii=False)[:400]) return r['result'] def show(rows, only=None): print(u'%-30s %-32s %-11s %-15s %s' % (u'器件', u'封装', u'立创', u'本体 w x h', u'框 y0..y1 判断')) for name, fp, lcsc, tr, _u, _l in rows: if only and only.lower() not in (name + fp).lower(): continue b = parse(tr) if b is None: print(u'%-30s %-32s %-11s —— 没有 3D 模型,读不出来' % (name[:30], fp[:32], lcsc)) continue w, h, x0, y0, x1, y1 = b # 立式 = 本体基本骑在焊盘上;弯针 = 整个甩到一边 kind = u'立式' if abs((y0 + y1) / 2) < h * 0.25 else u'弯针/偏置' print(u'%-30s %-32s %-11s %6.2f x %6.2f %+6.2f..%+6.2f %s' % (name[:30], fp[:32], lcsc, w, h, y0, y1, kind)) def check(): u"""拿 `pcb1_place.FP_BODY` 的手工实测值和 3D 模型对账。""" import pcb1_place as P hit = {} for fp in P.FP_BODY: kw = fp.split('_')[-1] or fp for row in search(kw, 40): if row[1] == fp and row[3]: hit[fp] = row break print(u'%-34s %-22s %-22s %s' % (u'封装', u'3D 框 y', u'FP_BODY 框 y', u'')) bad = 0 for fp, want in sorted(P.FP_BODY.items()): row = hit.get(fp) if not row: print(u'%-34s —— 没搜到带 3D 的器件,跳过' % fp[:34]) continue b = parse(row[3]) ok = abs(b[3] - want[1]) < 0.6 and abs(b[5] - want[3]) < 0.6 bad += 0 if ok else 1 print(u'%-34s %+7.2f..%+7.2f %+7.2f..%+7.2f %s' % (fp[:34], b[3], b[5], want[1], want[3], u'✔' if ok else u'✘ 对不上')) print(u'\n对不上的 %d 个' % bad) return bad if __name__ == '__main__': argv = sys.argv[1:] if '--check' in argv: sys.exit(1 if check() else 0) if '--scan' in argv: kw = argv[argv.index('--scan') + 1] only = argv[argv.index('--only') + 1] if '--only' in argv else None show(search(kw, 40), only) else: sys.exit(__doc__)