# -*- coding: utf-8 -*- u"""底板**位号丝印**的重排与下发。 /c/Python314/python .claude/r2/silk_base.py # 只算 + 体检(改前 / 改后对照) /c/Python314/python .claude/r2/silk_base.py --push # 下发(pcb_PrimitiveAttribute.modify)+ 读回核对 ## 判据 位号文字框(字高 `fs`、字宽 ≈ 0.62×字高×字符数,`alignMode=3` 居中)不许: ① 压同面焊盘(顶层丝印 layer 3 对 L1 + 通孔盘;底层 layer 4 对 L4 + 通孔盘) ② 压过孔 ③ 越板框(留 `EDGE` 0.5) ④ 与别的位号重叠 ⚠ **大件(本体 ≥ 5 mm)的位号保留原位**:五个插槽的位号故意放在槽体西侧 x=28(离本体 21 mm), 那是设计,不是问题;U20 / 电解 / 座子同理。它们先占位,小件绕开它们。 ⚠ 位号必须**紧挨器件**才有意义 ⇒ 离本体外框有硬上限 `MAXD`。够不着就降级: 理想位 → 允许压过孔 → 允许压焊盘 → 保留原位。单纯"找个空地"会把位号甩到十几毫米外。 ⚠ `rotation` 字段单位是**度且不归一化**(900 = 180°),读回要 `%% 360`。 """ import io import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) from eda import run # noqa: E402 import pcb_base as B # noqa: E402 import place_base as PL # noqa: E402 MIL = 0.0254 SNAP = os.path.join(HERE, '_base_silk.json') PLAN = os.path.join(HERE, '_base_silk_plan.json') CHAR_W, EDGE, GAP, MAXD = 0.62, 0.5, 0.15, 2.2 # ⛔ 保留原位的**显式名单**(位号故意远离本体,不是问题): # 五个插槽的位号放在槽体西侧 x=28(离本体 21 mm)、U20 让开天线净空、 # 顶面件与板边 USB-C 的位号只能朝板内放。 # ⚠ 别用「本体 ≥ 5 mm」当判据:那会把 R9A(7.4×3.5) / D3(6.8×2.2) / U13(2.3×5.8) # 这些**该重排**的中等件也豁免掉 —— 实测残留 25 个里有 18 个是这么来的。 KEEP = {'J34', 'J35', 'J36', 'J37', 'J38', 'U20', 'J26', 'J27', 'J33', 'C2', 'C3', 'J1', 'J23'} READ = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2500)); const M = 0.0254, nrm = v => ((v %% 360) + 360) %% 360; const A = (await eda.pcb_PrimitiveAttribute.getAll()||[]).filter(a=>a.key==='Designator') .map(a=>({ref:a.value, lay:a.layer, x:a.x*M, y:a.y*M, rot:nrm(a.rotation), fs:a.fontSize*M, vis:a.valueVisible?1:0, id:a.primitiveId})); const C = (await eda.pcb_PrimitiveComponent.getAll()||[]) .map(c=>({ref:c.designator, x:c.x*M, y:c.y*M, rot:nrm(c.rotation||0), lay:c.layer})); const V = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x*M, v.y*M, String(v.net), v.diameter*M]); return {attrs:A, comps:C, vias:V}; """ % B.PCB def snap(): r = run(READ, timeout=600) d = r.get('result') assert d, json.dumps(r, ensure_ascii=False)[:400] io.open(SNAP, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False)) return d def tbox(ref, x, y, rot, fs): h = fs w = CHAR_W * h * len(ref) if rot in (90, 270): w, h = h, w return (x - w / 2., y - h / 2., x + w / 2., y + h / 2.) def ov(p, q): return not (p[2] <= q[0] or q[2] <= p[0] or p[3] <= q[1] or q[3] <= p[1]) def plan(d, pads, sz): comp = {c['ref']: c for c in d['comps']} V = d['vias'] padsL = {3: [p for p in pads if p[6] in (1, 12)], 4: [p for p in pads if p[6] in (2, 12)]} def hit_pad(bb, lay): return any(ov(bb, (p[2] - p[4] / 2., p[3] - p[5] / 2., p[2] + p[4] / 2., p[3] + p[5] / 2.)) for p in padsL[lay]) def hit_via(bb): return any(ov(bb, (v[0] - v[3] / 2., v[1] - v[3] / 2., v[0] + v[3] / 2., v[1] + v[3] / 2.)) for v in V) def outside(bb): return bb[0] < EDGE or bb[1] < EDGE or bb[2] > B.BW - EDGE or bb[3] > B.BH - EDGE placed, out, stat = [], {}, {} for a in d['attrs']: if a['vis'] and a['ref'] in KEEP: placed.append((a['lay'], tbox(a['ref'], a['x'], a['y'], a['rot'], a['fs']))) stat[u'白名单保留'] = stat.get(u'白名单保留', 0) + 1 small = [a for a in d['attrs'] if a['vis'] and a['ref'] not in KEEP] small.sort(key=lambda a: -(sz.get(a['ref'], (1, 1))[0] * sz.get(a['ref'], (1, 1))[1])) for a in small: ref, c, lay, fs = a['ref'], comp.get(a['ref']), a['lay'], a['fs'] if not c: continue w, h = sz.get(ref, (1.0, 1.0)) if int(round(c['rot'])) % 180: w, h = h, w best, level = None, None for lv in (0, 1, 2): r = 0.0 while r <= MAXD and best is None: for ang in (90, 270, 0, 180, 45, 135, 225, 315): for rot in ((0, 90) if ang in (0, 180) else (0,)): tw, th = CHAR_W * fs * len(ref), fs if rot == 90: tw, th = th, tw ca, sa = math.cos(math.radians(ang)), math.sin(math.radians(ang)) if abs(ca) <= 0.7 and abs(sa) <= 0.7: off = math.hypot(w / 2. + tw / 2., h / 2. + th / 2.) + GAP + r elif abs(ca) > 0.7: off = w / 2. + tw / 2. + GAP + r else: off = h / 2. + th / 2. + GAP + r x, y = c['x'] + off * ca, c['y'] + off * sa bb = tbox(ref, x, y, rot, fs) if outside(bb) or any(b[0] == lay and ov(bb, b[1]) for b in placed): continue if lv < 2 and hit_pad(bb, lay): continue if lv < 1 and hit_via(bb): continue best, level = (round(x, 4), round(y, 4), rot, bb), lv break if best: break r += 0.25 if best: break if best: placed.append((lay, best[3])) out[ref] = {'x': best[0], 'y': best[1], 'rot': best[2], 'id': a['id'], 'lay': lay} k = [u'理想位', u'允许压过孔', u'允许压焊盘'][level] stat[k] = stat.get(k, 0) + 1 else: stat[u'无解(保留原位)'] = stat.get(u'无解(保留原位)', 0) + 1 return out, stat def main(): d = snap() pads = json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8')) sz = PL.sizes() p, stat = plan(d, pads, sz) io.open(PLAN, 'w', encoding='utf-8').write(json.dumps(p, ensure_ascii=False, indent=0)) print(u'位号 %d(可见 %d);重排 %d 个' % (len(d['attrs']), sum(a['vis'] for a in d['attrs']), len(p))) for k, v in sorted(stat.items(), key=lambda t: -t[1]): print(u' %-14s %d' % (k, v)) if '--push' not in sys.argv: print(u'(只算;加 --push 下发)') return rows = [[v['id'], v['x'] / MIL, v['y'] / MIL, v['rot']] for v in p.values()] n = 0 for k in range(0, len(rows), 60): r = run(u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1500)); const R = %s; let n = 0; for (const [id, x, y, rot] of R) { try { await eda.pcb_PrimitiveAttribute.modify(id, {x: x, y: y, rotation: rot}); n++; } catch (e) {} } return n; """ % (B.PCB, json.dumps(rows[k:k + 60])), timeout=600) n += r.get('result') or 0 run(u"await eda.pcb_Document.save(); return 1;", timeout=300) print(u' modify 成功 %d / %d' % (n, len(rows))) # 读回核对 d2 = 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(now[k]['rot']) % 360 != v['rot'] % 360] print(u'读回核对:%d 个对上,%d 个没对上 %s' % (len(p) - len(bad), len(bad), bad[:10])) if __name__ == '__main__': main()