板上 pcb_PrimitiveString 原本 0 条 —— 方案 §5.2 要求的插槽警告、J1/J23 的接口标注 一直只写在原理图说明文字里,从没落到 PCB。 已补并读回核对 6/6: - PWR 20V L4 (12.500, 1.800) J1 与 J23 同一颗 USB-C 座,插错要能看出来 - USB L4 (84.250, 1.800) J23 是 Hub 上行口 - NOT PCIe - PhoneRail blade only L3 (49.000, 79.480) - NOT PCIe ×3 L3 (36.000,18.520) / (62.000,38.840) / (45.000,59.160) 完整句只放得下一条,其余槽间放短标 ⇒ 五个槽每个都邻着一条。 三条坑(已写进 R2 §39.5): - ⛔ 丝印压铜不算冲突(铜上有阻焊)。第一版把压铜也当冲突,六个候选位全军覆没, 看着像「板上没地方放字」。真正要避的只有 焊盘 / 过孔 / 位号 / 安装孔 / 板框外。 - ⚠ alignMode 要显式传 5(CENTER),不传默认不是居中,字会整体偏出去; 位号 Attribute 用的 3 是另一套口径。 - ⚠ 核对不能拿文案当字典 key:NOT PCIe 有三条,同名互相覆盖 ⇒ 「6 条都建上了」却报成「4/6 对上」。按 文案+坐标+层 配对。 新工具 silk_str.py:自带落点搜索(0.25 步长 + 偏移上限)、幂等下发、--verify 只读核对。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
196 行
8.3 KiB
Python
196 行
8.3 KiB
Python
# -*- 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()
|