# -*- coding: utf-8 -*- u"""底板的**自由丝印**(`pcb_PrimitiveString`):接口标注 + 插槽警告。 /c/Python314/python .claude/r2/silk_str.py # 抓板 + 找落点 + 报告(不下发) /c/Python314/python .claude/r2/silk_str.py --push # 下发(幂等:先删同文案的,再建)+ 读回核对 /c/Python314/python .claude/r2/silk_str.py --verify # 只读回核对 ## 要标什么(方案 §5.2 / A1B-PD 的 note / PROGRESS 待办) | 文案 | 层 | 为什么 | |---|---|---| | `PWR 20V` | 4(底层,J1 在底面)| J1 与 J23 是**同一颗 USB-C 座**,插错要能看出来 | | `USB` | 4 | 同上,J23 是 Hub 上行口 | | `NOT PCIe - PhoneRail blade only` | 3(顶层,插槽在顶面)| 槽是 PCI-E x4 座,但针位是自定义的;插真 PCIe 卡会烧 | | `NOT PCIe` ×3 | 3 | 五个槽每个都要邻着一条提示,完整句只放得下一条 | ## 判据:丝印**只避** 焊盘 / 过孔 / 位号 / 安装孔 / 板框外 ⛔ **压铜是常态**,不算冲突 —— 铜上有阻焊,油墨正常印在阻焊上。第一版把「压铜」也当冲突, 于是六个候选位全军覆没,看着像「板上没地方放字」。 ⚠ `alignMode` 要显式传 **5 = CENTER**(枚举 `EPCB_PrimitiveStringAlignMode`)。 不传的话默认不是居中,坐标含义和这里按中心算的框对不上,字会整体偏出去。 (位号 `Attribute` 用的 3 是另一套口径,别照抄。) """ 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 fp_edge # noqa: E402 MIL = fp_edge.MIL FS, LW = 1.2, 0.15 # 字高 / 线宽(嘉立创丝印下限 0.8 / 0.15) CHAR_W, EDGE, CLR = 0.62, 0.5, 0.10 ALIGN_CENTER = 5 PLAN = os.path.join(HERE, '_base_silk_str.json') # (文案, 层, 目标 x, 目标 y, 允许偏移 dx, dy) SILK = [ (u'PWR 20V', 4, 12.5, 1.8, 4.0, 1.0), # J1(底面)南侧 (u'USB', 4, 84.0, 1.8, 4.0, 1.0), # J23(底面)南侧 (u'NOT PCIe - PhoneRail blade only', 3, 49.0, 79.48, 8.0, 3.0), # 槽4~槽5 之间 (u'NOT PCIe', 3, 36.0, 18.52, 10.0, 3.0), # 槽1~槽2 (u'NOT PCIe', 3, 62.0, 38.84, 10.0, 3.0), # 槽2~槽3 (u'NOT PCIe', 3, 49.0, 59.16, 16.0, 4.0), # 槽3~槽4 ] READ = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2500)); const M = 0.0254; const V = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x*M, v.y*M, v.diameter*M]); const A = (await eda.pcb_PrimitiveAttribute.getAll()||[]).filter(a=>a.key==='Designator' && a.valueVisible) .map(a=>({ref:a.value, lay:a.layer, x:a.x*M, y:a.y*M, rot:((a.rotation%%360)+360)%%360, fs:a.fontSize*M})); const S = (await eda.pcb_PrimitiveString.getAll()||[]) .map(s=>({lay:s.layer, x:s.x*M, y:s.y*M, t:(s.text!==undefined?s.text:s.string), id:s.primitiveId})); return {vias:V, attrs:A, strs:S}; """ % B.PCB def tbox(t, x, y, fs=FS): h = fs w = CHAR_W * h * len(t) return (x - w / 2. - CLR, y - h / 2. - CLR, x + w / 2. + CLR, y + h / 2. + CLR) 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 clean(t, lay, x, y, pads, vias, attrs): bb = tbox(t, x, y) if bb[0] < EDGE or bb[1] < EDGE or bb[2] > B.BW - EDGE or bb[3] > B.BH - EDGE: return False want = (1, 12) if lay == 3 else (2, 12) for p in pads: if p[6] in want and _ov(bb, (p[2] - p[4] / 2., p[3] - p[5] / 2., p[2] + p[4] / 2., p[3] + p[5] / 2.)): return False for (vx, vy, vd) in vias: if _ov(bb, (vx - vd / 2., vy - vd / 2., vx + vd / 2., vy + vd / 2.)): return False for a in attrs: if a['lay'] != lay: continue h = a['fs'] w = CHAR_W * h * len(a['ref']) if a['rot'] in (90, 270): w, h = h, w if _ov(bb, (a['x'] - w / 2., a['y'] - h / 2., a['x'] + w / 2., a['y'] + h / 2.)): return False r = B.HOLE_D / 2. + 0.6 for (hx, hy) in B.MOUNT_HOLES.values(): if _ov(bb, (hx - r, hy - r, hx + r, hy + r)): return False return True def fit(t, lay, x0, y0, dx, dy, pads, vias, attrs): r = 0.0 while r <= max(dx, dy): for sy in (0, 1, -1): for sx in (0, 1, -1): x = x0 + sx * r y = y0 + sy * r * (dy / max(dx, 1e-9)) if abs(x - x0) > dx or abs(y - y0) > dy: continue if clean(t, lay, x, y, pads, vias, attrs): return round(x, 3), round(y, 3) r += 0.25 return None def main(): r = run(READ, timeout=600) d = r.get('result') assert d, json.dumps(r, ensure_ascii=False)[:400] pads = json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8')) print(u'板上:过孔 %d / 可见位号 %d / 自由丝印 %d' % (len(d['vias']), len(d['attrs']), len(d['strs']))) plan, miss = [], [] for (t, lay, x0, y0, dx, dy) in SILK: g = fit(t, lay, x0, y0, dx, dy, pads, d['vias'], d['attrs']) if g: plan.append({'text': t, 'layer': lay, 'x': g[0], 'y': g[1], 'fs': FS, 'lw': LW}) print(u' %-34s L%d (%7.3f, %7.3f) 偏离目标 %.2f' % (t, lay, g[0], g[1], math.hypot(g[0] - x0, g[1] - y0))) else: miss.append(t) print(u' %-34s L%d ⛔ 找不到干净落点' % (t, lay)) io.open(PLAN, 'w', encoding='utf-8').write(json.dumps(plan, ensure_ascii=False, indent=1)) if miss: sys.exit(u'!! %d 条没落点,不下发' % len(miss)) if '--verify' in sys.argv: verify(plan) return if '--push' not in sys.argv: print(u'(只算;加 --push 下发)') return # 幂等:先删掉板上文案相同的,再建 kill = [s['id'] for s in d['strs'] if s['t'] in [p['text'] for p in plan]] rows = [[p['layer'], p['x'] * MIL, p['y'] * MIL, p['text'], p['fs'] * MIL, p['lw'] * MIL] for p in plan] res = run(u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2500)); const KILL = %s, R = %s; if (KILL.length) { try { await eda.pcb_PrimitiveString.delete(KILL); } catch (e) {} } let n = 0; for (const [lay, x, y, t, fs, lw] of R) { const o = await eda.pcb_PrimitiveString.create(lay, x, y, t, undefined, fs, lw, %d, 0); if (o) n++; } await eda.pcb_Document.save(); return {deleted: KILL.length, made: n, now: (await eda.pcb_PrimitiveString.getAll()||[]).length}; """ % (B.PCB, json.dumps(kill), json.dumps(rows, ensure_ascii=False), ALIGN_CENTER), timeout=600) print(u'下发:%s' % json.dumps(res.get('result'), ensure_ascii=False)) verify(plan) def verify(plan): u"""读回逐条核对。 ⛔ **不能用文案当字典 key** —— `NOT PCIe` 有三条,同名会互相覆盖, 于是「6 条都建上了」却报成「4/6 对上」。按**文案 + 坐标**配对,配上一条就消费掉一条。""" back = run(READ, timeout=600).get('result') or {} pool = list(back.get('strs', [])) bad = [] for q in plan: hit = None for k, s in enumerate(pool): if s['t'] == q['text'] and abs(s['x'] - q['x']) <= 0.05 and abs(s['y'] - q['y']) <= 0.05 and s['lay'] == q['layer']: hit = k break if hit is None: bad.append(u'%s @ (%.3f, %.3f) L%d' % (q['text'], q['x'], q['y'], q['layer'])) else: pool.pop(hit) print(u'读回核对:%d / %d 条对上;板上自由丝印共 %d 条%s' % (len(plan) - len(bad), len(plan), len(back.get('strs', [])), (u';没对上:%s' % u' / '.join(bad)) if bad else u'')) return not bad if __name__ == '__main__': main()