# -*- coding: utf-8 -*- u"""给 R900~R909 就近找合法位置 —— 用布线器自己的障碍表搜,不靠肉眼看空地。 /c/Python314/python .claude/pcb2_strap_place.py # 搜 + 打印建议坐标 ## 为什么要重摆 第一版把十颗全摆在 U32 北边 y=79 那一排(那里确实是唯一的整块空地), 结果 `pcb2_route_sig.py` **10 条只布通 3 条** —— 南边(pin 49/50/51)、西边(15/17)、 东南(47/48)那七个脚要绕过整颗 QFN 和一圈去耦,绕不出去。 ⇒ 改成**每颗贴着自己的引脚放**。空地是按障碍表搜出来的: 以引脚为心一圈圈往外找,第一个「两个焊盘都离异网铜 >= 0.20 mm」的位置就用。 判据和 `pcb2_clear.py` / 扇出用的是同一套(`pcb2_route_dp.seg_rect` + `CLR`)。 """ import io import json import math import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run import pcb2_route_dp as D import pcb2_keypads as K CLR = D.CLR PW, PH = 0.5664, 0.5410 # R0402 单个焊盘 mm(22.3 x 21.3 mil) POFF = 0.433 # 焊盘中心离元件中心 BODY = 0.55 # 本体半长,用来判两颗新件之间 # 位号 -> U32 脚号 MAP = [('R906', '15'), ('R905', '17'), ('R904', '31'), ('R903', '33'), ('R909', '34'), ('R902', '47'), ('R908', '48'), ('R901', '49'), ('R907', '50'), ('R900', '51')] def call(js, timeout=240): r = run("await eda.dmt_EditorControl.openDocument('942ed1560d81c1e7');" "await new Promise(r=>setTimeout(r,2000));" + js, timeout=timeout) if not r.get('success'): sys.exit(u'桥接失败: ' + json.dumps(r, ensure_ascii=False)[:600]) return r['result'] def u32_pins(): return call(""" let pid = null; for (const c of await eda.pcb_PrimitiveComponent.getAll()) if (c.getState_Designator() === 'U32') pid = c.getState_PrimitiveId(); const M = 39.3700787401575, out = {}; for (const p of await eda.pcb_PrimitivePad.getAll()) { if ((p.primitiveId || '').indexOf(pid) !== 0) continue; out[String(p.getState_PadNumber ? p.getState_PadNumber() : p.number)] = [Math.round(p.getState_X()/M*1000)/1000, Math.round(p.getState_Y()/M*1000)/1000]; } return out;""") def pad_rects(cx, cy, rot): u"""返回 [pad1(GND), pad2(strap)]。rot 0/90/180/270 四个方向都要给 —— ⚠️ 第一版只搜 0 和 90,等于默认「GND 端朝西/朝南」,结果 strap 线要跨过自己的 GND 焊盘 + GND 短线 + GND 过孔才够得着 U32 的脚,十条里九条过不了净距校核。""" r = rot % 360 if r == 0: return [(cx - POFF, cy, PW / 2, PH / 2), (cx + POFF, cy, PW / 2, PH / 2)] if r == 180: return [(cx + POFF, cy, PW / 2, PH / 2), (cx - POFF, cy, PW / 2, PH / 2)] if r == 90: return [(cx, cy - POFF, PH / 2, PW / 2), (cx, cy + POFF, PH / 2, PW / 2)] return [(cx, cy + POFF, PH / 2, PW / 2), (cx, cy - POFF, PH / 2, PW / 2)] def bodies(pads, mine): u"""⚠️ 障碍表只有焊盘/过孔/导线,**不知道元件本体**。第一版没这一条, 搜索结果里 R906/R909 直接落进了 U32 那颗 QFN 的本体底下(焊盘确实没撞,物理上装不了)。 这里用「同一个 ref 的所有焊盘的包围盒」当本体,QFN/SOIC 都够准。""" bb = {} for q in pads: if q['ref'] in mine or not q['ref']: continue b = bb.setdefault(q['ref'], [1e9, 1e9, -1e9, -1e9]) b[0] = min(b[0], q['x'] - q['hw']); b[1] = min(b[1], q['y'] - q['hh']) b[2] = max(b[2], q['x'] + q['hw']); b[3] = max(b[3], q['y'] + q['hh']) return [(r, b) for r, b in bb.items()] def clear(cx, cy, rot, pads, vias, lines, mine, taken, bods): u"""两个焊盘离**所有**异网铜 >= CLR(GND 也算,0402 本体不能骑在 GND 盘上), 而且不许落进任何已有元件的本体投影。""" for px, py, hw, hh in pad_rects(cx, cy, rot): for ref, b in bods: if b[0] - CLR < px + hw and px - hw < b[2] + CLR and b[1] - CLR < py + hh and py - hh < b[3] + CLR: return False, u'%s 本体' % ref for px, py, hw, hh in pad_rects(cx, cy, rot): for q in pads: if q['ref'] in mine: continue if D.seg_rect((px, py), (px, py), q['x'], q['y'], q['hw'], q['hh']) < hw + hh + CLR: return False, u'焊盘 %s.%s(%s)' % (q['ref'], q['num'], q['net']) for v in vias: if math.hypot(v['x'] - px, v['y'] - py) < max(hw, hh) + v['r'] + CLR: return False, u'过孔(%s)' % v['net'] for l in lines: if l['lay'] not in (2, 12): continue d = D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py) if d < max(hw, hh) + l['w'] / 2 + CLR: return False, u'导线(%s)' % l['net'] # ⚠️ 新件之间也要按**真实焊盘矩形**判,不能用「中心距 > 本体」这种糙判据 —— # 第一版用 ±1.3 mm 的方框,push 之后 pcb2_clear 立刻抓出三对 0.03~0.15 mm。 for tx, ty, trot in taken: for ax, ay, ahw, ahh in pad_rects(cx, cy, rot): for bx, by, bhw, bhh in pad_rects(tx, ty, trot): dx = abs(ax - bx) - (ahw + bhw) dy = abs(ay - by) - (ahh + bhh) d = math.hypot(max(dx, 0.0), max(dy, 0.0)) if (dx > 0 or dy > 0) else max(dx, dy) if d < CLR: return False, u'挨着另一颗新件 (%.3f)' % d return True, '' def gnd_via_ok(cx, cy, rot, pads, vias, lines, mine, placed): u"""⚠️ 摆得下 ≠ 接得出去:地端还得能落一个合法过孔到 Inner2。 第一版没这一条,R903/R906 摆完之后地过孔被 NONREM0 / USB5_DM 围死, `pcb2_strap_gnd.py` 直接报「找不到扇出位置」。现在把它并进摆位判据。 返回 (可行, 过孔坐标)。""" gx, gy = pad_rects(cx, cy, rot)[0][:2] # pad1 = GND 端 for dist in [d / 10.0 for d in range(9, 31)]: for a in range(0, 360, 15): th = math.radians(a) vx, vy = round(gx + math.cos(th) * dist, 3), round(gy + math.sin(th) * dist, 3) if any(math.hypot(vx - ax, vy - ay) < 0.33 + K.VIA_H for ax, ay in placed): continue ok, _ = K._clear(vx, vy, K.VIA_D / 2, 12, pads, vias, lines, None) if not ok: continue ok, _ = K._seg_clear((gx, gy), (vx, vy), K.W_STUB, pads, vias, lines, None) if ok: return True, (vx, vy) return False, None def main(): if '--nodump' not in sys.argv: D.dump() pads, vias, lines = D.geom() pins = u32_pins() mine = {d for d, _ in MAP} bods = bodies(pads, mine) taken, out, gvias = [], {}, [] for des, pin in MAP: px, py = pins[pin] best, why = None, [] for r in [x / 10.0 for x in range(12, 81, 2)]: for a in range(0, 360, 10): th = math.radians(a) cx, cy = round(px + r * math.cos(th), 3), round(py + r * math.sin(th), 3) for rot in (0, 90, 180, 270): # ⭐ strap 端(pad2)必须比 GND 端(pad1)更靠近 U32 的脚, # 否则那条 strap 线只能绕过自己的 GND 焊盘/短线/过孔 —— 走不通。 g, t = pad_rects(cx, cy, rot) if math.hypot(t[0] - px, t[1] - py) >= math.hypot(g[0] - px, g[1] - py): continue ok, msg = clear(cx, cy, rot, pads, vias, lines, mine, taken, bods) if ok: vok, vxy = gnd_via_ok(cx, cy, rot, pads, vias, lines, mine, gvias) if not vok: if len(why) < 6: why.append(u'(%.2f,%.2f)r%d 地过孔出不去' % (cx, cy, rot)) continue best = (cx, cy, rot, r, vxy) gvias.append(vxy) break if len(why) < 6: why.append(u'(%.2f,%.2f)r%d 撞 %s' % (cx, cy, rot, msg)) if best: break if best: break if not best: print(u'!! %s (U32.%s) 找不到位置' % (des, pin)) for w in why: print(u' ' + w) sys.exit(1) out[des] = best taken.append((best[0], best[1], best[2])) print(u' %-5s U32.%-3s 脚(%7.3f,%6.3f) -> (%7.3f,%6.3f) rot%-3d 距 %.1f mm 地过孔(%7.3f,%6.3f)' % (des, pin, px, py, best[0], best[1], best[2], best[3], best[4][0], best[4][1])) io.open(os.path.join(HERE, '_pcb2_strap_place.json'), 'w', encoding='utf-8').write( json.dumps(out, ensure_ascii=False, indent=1)) print(u'\n粘到 pcb2_place.GLOBAL 里:') for des, pin in MAP: cx, cy, rot, r = out[des][:4] print(u" '%s': (%.3f, %.3f, %d), # U32.%s" % (des, cx, cy, rot, pin)) main()