文件
zhou yang和Claude Opus 5 312089326e R2 底板:按用户布局重布风扇窗口 + L1 走廊重开 + 位号丝印重排
布线(窗口整体重布,已下发):
- 拆窗口内 43 段(用户手布的那批,用户选「拆掉重来」),补 139 段 / 7 孔,
  GND 回流孔 10 个;fan_check 对板上真实快照逐项净距达标(含「新孔 vs 内层线」)。
- ⛔ 根因:pcb_base.PD_L1 里的 L1 走廊是按**旧摆位**开的(注释坐标全是旧的),
  用户重摆后与件对不上 ⇒ 四条长途网必然布不通。重开六条走廊后未完成 4 → 2。
  走廊必须盖住焊盘正上方,只盖沿途不行。断面:几何 11.80 / 铺铜后实测 9.82(要 8.0)。
- 与拆线前逐网对比:没引入任何新的连通性问题(GND 未接 58→48、+3V3 2→0、
  两路 TACH 2/1→0、+12V 一字不差)。

定版:板上铜以**用户自己布好的那版**为准,不再改动。

丝印(silk_base.py,已下发并读回核对 150/150):
- 压焊盘 128→4、压过孔 48→8、越框 8→3、互相重叠 34 对→0;字号不变。
- 三条坑:rotation 是度且不归一化(900=180°);别拿「本体≥5mm」当保留白名单
  (会豁免掉 R9A/D3/U13 这些该重排的件);「离本体」必须设硬上限 + 降级策略。

新工具:silk_base.py(丝印重排)、l1_section.py(L1 12V 主块逐 y 断面核算)、
fan_rip.py(按窗口拆线,拆前后比对全板线/孔总数防连带删除)。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-12 12:02:32 +08:00

191 行
8.3 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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()