# -*- coding: utf-8 -*- u"""BLADE-PCB_1(2026-09-15 新摆位 + 上拉电阻挪到 U4 东南之后)的**位号丝印重排**。 /c/Python314/python .claude/r2/silk_blade1.py # 只算 + 体检 /c/Python314/python .claude/r2/silk_blade1.py --push # 下发 + 读回核对 复用 `silk_blade.py` 的快照 / 文字框 / 板框与禁区判据 / 下发,但**换了排位器**: `silk_blade.plan()` 只看焊盘和过孔,不看**别的元件本体** ⇒ 在新摆位上把 C11 的位号放到了 C6 的本体上 (离 C6 中心 0.29)、C12 放到 F1 上 —— 体检四项全 0,但读板的人会认错件。 这里的判据(按优先级): 硬约束:不越框 / 不入金手指区 / 不压任何焊盘(外扩 0.1)/ 不与已放位号重叠 软约束(计分):到本元件本体的距离 + 压别的元件本体 3.0 + 每压一个过孔 1.5 候选:本体四周 0.1 mm 网格、±3.0 mm、字 0° / 90°;大件先排。 J1 位号手工定在 (43.0, 10.5):金手指居中后原来的 (40.28, 8.25) 落进台阶以下、压着地母线孔。 """ import io import json import math import sys import silk_blade as S # noqa: E402 (同目录,会顺带配好 sys.path) import pcb_place as PL # noqa: E402 RANGE, STEP = 3.0, 0.1 J1_POS = (43.0, 10.5, 0) def bodies(d, pads, sz): u"""{位号: 本体框}:焊盘外包框与 pcb_place 本体尺寸取大,外扩 0.1。""" by = {} for q in pads: if q[6] == 12: continue b = by.setdefault(q[0], [1e9, 1e9, -1e9, -1e9]) b[0] = min(b[0], q[2] - q[4] / 2.) b[1] = min(b[1], q[3] - q[5] / 2.) b[2] = max(b[2], q[2] + q[4] / 2.) b[3] = max(b[3], q[3] + q[5] / 2.) out = {} for c in d['comps']: r = c['ref'] w, h = sz.get(r, (0.6, 0.6)) if int(round(c['rot'])) % 180: w, h = h, w b = by.get(r, [c['x'], c['y'], c['x'], c['y']]) out[r] = (min(b[0], c['x'] - w / 2.) - 0.1, min(b[1], c['y'] - h / 2.) - 0.1, max(b[2], c['x'] + w / 2.) + 0.1, max(b[3], c['y'] + h / 2.) + 0.1, c['lay']) return out def box_dist(a, b): dx = max(b[0] - a[2], a[0] - b[2], 0.0) dy = max(b[1] - a[3], a[1] - b[3], 0.0) return math.hypot(dx, dy) def plan(d, pads, sz): attrs = {a['ref']: a for a in d['attrs'] if a['vis']} body = bodies(d, pads, sz) # 丝印层 3 = 顶、4 = 底;元件层 1 = 顶、2 = 底 side = {3: 1, 4: 2} padsL = S._pads_by_layer(pads) out, placed, stat = {}, [], {} j = attrs.get('J1') if j: out['J1'] = {'x': J1_POS[0], 'y': J1_POS[1], 'rot': J1_POS[2], 'id': j['id'], 'lay': j['lay']} placed.append((j['lay'], S.tbox('J1', *J1_POS, fs=j['fs']))) order = sorted([r for r in attrs if r != 'J1' and r in body], key=lambda r: -((body[r][2] - body[r][0]) * (body[r][3] - body[r][1]))) for r in order: a = attrs[r] ob = body[r] lay = a['lay'] others = [b for k, b in body.items() if k != r and b[4] == side.get(lay)] best = None cx, cy = (ob[0] + ob[2]) / 2., (ob[1] + ob[3]) / 2. hw, hh = (ob[2] - ob[0]) / 2. + RANGE, (ob[3] - ob[1]) / 2. + RANGE nx, ny = int(hw / STEP), int(hh / STEP) for rot in (0, 90): for i in range(-nx, nx + 1): x = cx + i * STEP for jj in range(-ny, ny + 1): y = cy + jj * STEP bb = S.tbox(r, x, y, rot, a['fs']) dist = box_dist(bb, ob) if dist > RANGE or S.ov(bb, ob[:4]): continue if best is not None and dist >= best[0]: continue if S.outside(bb) or any(L == lay and S.ov(bb, b) for L, b in placed): continue if any(S.ov(bb, (q[2] - q[4] / 2. - 0.1, q[3] - q[5] / 2. - 0.1, q[2] + q[4] / 2. + 0.1, q[3] + q[5] / 2. + 0.1)) for q in padsL.get(lay, [])): continue nb = sum(1 for b in others if S.ov(bb, b[:4])) nv = sum(1 for v in d['vias'] if S.ov(bb, (v[0] - v[3] / 2., v[1] - v[3] / 2., v[0] + v[3] / 2., v[1] + v[3] / 2.))) sc = dist + 3.0 * nb + 1.5 * nv if best is None or sc < best[0]: best = (sc, x, y, rot, nb, nv, dist) if best: placed.append((lay, S.tbox(r, best[1], best[2], best[3], a['fs']))) out[r] = {'x': round(best[1], 4), 'y': round(best[2], 4), 'rot': best[3], 'id': a['id'], 'lay': lay, 'nb': best[4], 'nv': best[5], 'dist': round(best[6], 2)} k = u'干净' if not best[4] and not best[5] else (u'压别的本体' if best[4] else u'压过孔') stat[k] = stat.get(k, 0) + 1 else: stat[u'无解'] = stat.get(u'无解', 0) + 1 print(u' [!] %s 无解,保留原位' % r) return out, stat def main(): d = S.snap() pads = json.load(io.open(S.PADS, encoding='utf-8')) sz = PL.sizes() vis = [a for a in d['attrs'] if a['vis']] before = [(a['ref'], a['lay'], S.tbox(a['ref'], a['x'], a['y'], a['rot'], a['fs'])) for a in vis] print(u'改前体检: %s' % json.dumps(S.audit(before, pads, d['vias']), ensure_ascii=False)) p, stat = plan(d, pads, sz) print(u'重排 %d 个: %s' % (len(p), json.dumps(stat, ensure_ascii=False))) for r, q in sorted(p.items()): if q.get('nb') or q.get('nv') or q.get('dist', 0) > 1.5: print(u' %-4s (%.2f, %.2f) rot %d 离本体 %.2f 压本体 %d 压孔 %d' % (r, q['x'], q['y'], q['rot'], q.get('dist', 0), q.get('nb', 0), q.get('nv', 0))) after = [(a['ref'], a['lay'], S.tbox(a['ref'], p.get(a['ref'], a)['x'], p.get(a['ref'], a)['y'], p.get(a['ref'], a)['rot'], a['fs'])) for a in vis] print(u'改后体检: %s' % json.dumps(S.audit(after, pads, d['vias']), ensure_ascii=False)) io.open(S.PLAN, 'w', encoding='utf-8').write(json.dumps(p, ensure_ascii=False, indent=0)) if '--push' not in sys.argv: print(u'(只算;加 --push 下发)') return rows = [[v['id'], v['x'] / S.MIL, v['y'] / S.MIL, v['rot']] for v in p.values()] n = 0 for k in range(0, len(rows), 60): r = S.run(S.PUSH % (S.B.PCB, json.dumps(rows[k:k + 60])), timeout=600) n += r.get('result') or 0 S.run(u"await eda.pcb_Document.save(); return 1;", timeout=300) print(u'modify 成功 %d / %d' % (n, len(rows))) d2 = S.snap() now = {a['ref']: a for a in d2['attrs']} bad = [k for k, v in p.items() if k not in now or abs(now[k]['x'] - v['x']) > 0.02 or abs(now[k]['y'] - v['y']) > 0.02 or int(round(now[k]['rot'])) % 360 != v['rot'] % 360] print(u'读回核对:%d 个对上,%d 个没对上 %s' % (len(p) - len(bad), len(bad), bad[:10])) if __name__ == '__main__': main()