R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,149 @@
|
||||
# -*- 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()
|
||||
在新工单中引用
屏蔽一个用户