R2 底板:补 6 条自由丝印(插槽 NOT PCIe 警告 + J1/J23 接口标注)

板上 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
这个提交包含在:
zhou yang
2026-09-12 12:48:15 +08:00
共同撰写人 Claude Opus 5
父节点 dc4f9b9d1c
当前提交 22d9f31e9b
共修改 4 个文件,包含 294 行新增和 0 行删除
+50
查看文件
@@ -0,0 +1,50 @@
[
{
"text": "PWR 20V",
"layer": 4,
"x": 12.5,
"y": 1.8,
"fs": 1.2,
"lw": 0.15
},
{
"text": "USB",
"layer": 4,
"x": 84.25,
"y": 1.8,
"fs": 1.2,
"lw": 0.15
},
{
"text": "NOT PCIe - PhoneRail blade only",
"layer": 3,
"x": 49.0,
"y": 79.48,
"fs": 1.2,
"lw": 0.15
},
{
"text": "NOT PCIe",
"layer": 3,
"x": 36.0,
"y": 18.52,
"fs": 1.2,
"lw": 0.15
},
{
"text": "NOT PCIe",
"layer": 3,
"x": 62.0,
"y": 38.84,
"fs": 1.2,
"lw": 0.15
},
{
"text": "NOT PCIe",
"layer": 3,
"x": 45.0,
"y": 59.16,
"fs": 1.2,
"lw": 0.15
}
]
+195
查看文件
@@ -0,0 +1,195 @@
# -*- 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()