R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,255 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""D16 南带补线结果的本机复核(不跑任何搜索,只算几何):
|
||||
|
||||
/c/Python314/python .claude/r2/d16_check.py [结果文件,默认 _d16_route.out.json] [--list N]
|
||||
|
||||
① 精确净距:新增的每段线 / 每个孔,对板上快照(_board_for_nets.json,四层全算,含内层线)+ 焊盘
|
||||
(通孔算所有铜层)+ 封装 NPTH + 板边,按真实几何算最小距离,< MIN_CLR 就报(同网不算)。
|
||||
② L1 上的新线必须落在 pcb_base.PD_L1 的窗口里(西侧别处的 L1 是 12V 大铜,线会切断它)。
|
||||
③ 连通:逐网按「线-线 / 线-孔 / 线-盘 / 孔-盘」相碰合并,报新入口相关网还剩几块(与 route_nets 同口径;
|
||||
不含铺铜 —— GND 这类靠铺铜成网的只看有没有孔)。
|
||||
"""
|
||||
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, HERE)
|
||||
import pcb_base as B # noqa: E402
|
||||
|
||||
MIN_CLR = 0.15
|
||||
EDGE_CLR = 0.3
|
||||
VIA_HOLE = 0.3
|
||||
HOLE_CLR = 0.3 # 钻孔到钻孔(DRC otherClearance 11.8 mil),不分网
|
||||
WIN_TOL = 0.3 # L1 线出 PD_L1 窗口的容差(pour_base 挖 12V 时小岛外扩 0.5)
|
||||
LAYERS = (1, 15, 16, 2)
|
||||
NETS = ['PD_CC1', 'PD_CC2', 'SW12', 'BST12', 'VCC12', 'FB12', 'RT12', 'EN12', '#CFG1',
|
||||
'VIN_RAW', 'VIN_FUSED', 'VIN_SW', 'SDA', 'SCL', 'NTC2', 'INA_ALERT'] # #CFG1 = U36.9 的自动网名,按脚现取
|
||||
|
||||
|
||||
def pt_seg(px, py, ax, ay, bx, by):
|
||||
dx, dy = bx - ax, by - ay
|
||||
L = dx * dx + dy * dy
|
||||
t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L))
|
||||
return math.hypot(px - (ax + t * dx), py - (ay + t * dy))
|
||||
|
||||
|
||||
def seg_seg(a, b):
|
||||
(x1, y1, x2, y2), (x3, y3, x4, y4) = a, b
|
||||
d1, d2, d3, d4 = x2 - x1, y2 - y1, x4 - x3, y4 - y3
|
||||
den = d1 * d4 - d2 * d3
|
||||
if abs(den) > 1e-12:
|
||||
t = ((x3 - x1) * d4 - (y3 - y1) * d3) / den
|
||||
u = ((x3 - x1) * d2 - (y3 - y1) * d1) / den
|
||||
if 0 <= t <= 1 and 0 <= u <= 1:
|
||||
return 0.0
|
||||
return min(pt_seg(x1, y1, x3, y3, x4, y4), pt_seg(x2, y2, x3, y3, x4, y4),
|
||||
pt_seg(x3, y3, x1, y1, x2, y2), pt_seg(x4, y4, x1, y1, x2, y2))
|
||||
|
||||
|
||||
def pt_rect(px, py, r):
|
||||
return math.hypot(max(r[0] - px, px - r[2], 0.0), max(r[1] - py, py - r[3], 0.0))
|
||||
|
||||
|
||||
def seg_rect(s, r):
|
||||
x1, y1, x2, y2 = s
|
||||
if (r[0] <= x1 <= r[2] and r[1] <= y1 <= r[3]) or (r[0] <= x2 <= r[2] and r[1] <= y2 <= r[3]):
|
||||
return 0.0
|
||||
edges = [(r[0], r[1], r[2], r[1]), (r[2], r[1], r[2], r[3]), (r[2], r[3], r[0], r[3]), (r[0], r[3], r[0], r[1])]
|
||||
return min(seg_seg(s, e) for e in edges)
|
||||
|
||||
|
||||
def prect(p):
|
||||
return (p[2] - p[4] / 2, p[3] - p[5] / 2, p[2] + p[4] / 2, p[3] + p[5] / 2)
|
||||
|
||||
|
||||
def pad_layers(p):
|
||||
return LAYERS if p[6] == 12 else ((1,) if p[6] == 1 else (2,))
|
||||
|
||||
|
||||
def check_clearance(new_s, new_v, old_s, old_v, pads, npth):
|
||||
bad = []
|
||||
all_s = old_s + new_s
|
||||
all_v = old_v + new_v
|
||||
for s in new_s:
|
||||
l, x1, y1, x2, y2, w, n = s
|
||||
hw = w / 2.0
|
||||
for t in all_s:
|
||||
if t is s or t[6] == n or t[0] != l:
|
||||
continue
|
||||
d = seg_seg((x1, y1, x2, y2), t[1:5]) - hw - t[5] / 2.0
|
||||
if d < MIN_CLR - 1e-3:
|
||||
bad.append((d, u'线 %s L%s (%.3f,%.3f)-(%.3f,%.3f) ↔ 线 %s' % (n, l, x1, y1, x2, y2, t[6])))
|
||||
for v in all_v:
|
||||
if v[2] == n:
|
||||
continue
|
||||
d = pt_seg(v[0], v[1], x1, y1, x2, y2) - hw - v[3] / 2.0
|
||||
if d < MIN_CLR - 1e-3:
|
||||
bad.append((d, u'线 %s L%s (%.3f,%.3f)-(%.3f,%.3f) ↔ 孔 %s (%.2f,%.2f)'
|
||||
% (n, l, x1, y1, x2, y2, v[2], v[0], v[1])))
|
||||
for p in pads:
|
||||
if p[7] == n or l not in pad_layers(p):
|
||||
continue
|
||||
d = seg_rect((x1, y1, x2, y2), prect(p)) - hw
|
||||
if d < MIN_CLR - 1e-3:
|
||||
bad.append((d, u'线 %s L%s (%.3f,%.3f)-(%.3f,%.3f) ↔ 焊盘 %s.%s(%s)'
|
||||
% (n, l, x1, y1, x2, y2, p[0], p[1], p[7] or u'空')))
|
||||
for h in npth:
|
||||
d = pt_seg(h[0], h[1], x1, y1, x2, y2) - hw - h[3]
|
||||
if d < 0:
|
||||
bad.append((d, u'线 %s 压 NPTH (%.2f,%.2f)' % (n, h[0], h[1])))
|
||||
for (x, y) in ((x1, y1), (x2, y2)):
|
||||
e = min(x, y, B.BW - x, B.BH - y) - hw
|
||||
if e < EDGE_CLR:
|
||||
bad.append((e, u'线 %s 离板边 %.2f' % (n, e)))
|
||||
if l == 1 and min(x1, x2) < 28.0:
|
||||
# 按同网窗口的**并集**判(CC2 一根线横跨两块窗口);窗口外扩 WIN_TOL:router 封格子是向外取整的,
|
||||
# 线边会出窗口零点几,而 pour_base 挖 12V 大铜时小岛外扩了 0.5,这点余量不会碰到 12V 铜
|
||||
wins = [r for nn, r in B.PD_L1 if nn == n]
|
||||
k = max(2, int(math.hypot(x2 - x1, y2 - y1) / 0.05))
|
||||
for m in range(k + 1):
|
||||
px, py = x1 + (x2 - x1) * m / k, y1 + (y2 - y1) * m / k
|
||||
if not any(r[0] - WIN_TOL <= px - hw and px + hw <= r[2] + WIN_TOL and
|
||||
r[1] - WIN_TOL <= py - hw and py + hw <= r[3] + WIN_TOL for r in wins):
|
||||
bad.append((0.0, u'L1 线 %s (%.2f,%.2f)-(%.2f,%.2f) 出了 PD_L1 窗口(会切 12V 大铜)' % (n, x1, y1, x2, y2)))
|
||||
break
|
||||
for v in new_v:
|
||||
for t in all_s:
|
||||
if t[6] == v[2]:
|
||||
continue
|
||||
d = pt_seg(v[0], v[1], t[1], t[2], t[3], t[4]) - t[5] / 2.0 - v[3] / 2.0
|
||||
if d < MIN_CLR - 1e-3:
|
||||
bad.append((d, u'孔 %s (%.2f,%.2f) ↔ 线 %s L%s' % (v[2], v[0], v[1], t[6], t[0])))
|
||||
for u in all_v:
|
||||
if u is v:
|
||||
continue
|
||||
c = math.hypot(u[0] - v[0], u[1] - v[1])
|
||||
# 钻孔到钻孔**不分网**(DRC otherClearance 11.8 mil = 0.3):孔径 0.3 ⇒ 圆心距 ≥ 0.6;同网孔叠在一起也算
|
||||
if c - VIA_HOLE < HOLE_CLR - 1e-3 and not (u[2] == v[2] and c < 1e-3 and u is not v and False):
|
||||
bad.append((c - VIA_HOLE, u'孔 %s (%.2f,%.2f) ↔ 孔 %s (%.2f,%.2f) 钻孔间距 %.3f' % (v[2], v[0], v[1], u[2], u[0], u[1], c - VIA_HOLE)))
|
||||
continue
|
||||
if u[2] == v[2]:
|
||||
continue
|
||||
d = c - (u[3] + v[3]) / 2.0
|
||||
if d < MIN_CLR - 1e-3:
|
||||
bad.append((d, u'孔 %s (%.2f,%.2f) ↔ 孔 %s (%.2f,%.2f)' % (v[2], v[0], v[1], u[2], u[0], u[1])))
|
||||
for p in pads:
|
||||
if p[7] == v[2]:
|
||||
continue
|
||||
d = pt_rect(v[0], v[1], prect(p)) - v[3] / 2.0
|
||||
if d < MIN_CLR - 1e-3:
|
||||
bad.append((d, u'孔 %s (%.2f,%.2f) ↔ 焊盘 %s.%s(%s)' % (v[2], v[0], v[1], p[0], p[1], p[7] or u'空')))
|
||||
for h in npth:
|
||||
d = math.hypot(h[0] - v[0], h[1] - v[1]) - v[3] / 2.0 - h[3]
|
||||
if d < 0:
|
||||
bad.append((d, u'孔 %s 压 NPTH (%.2f,%.2f)' % (v[2], h[0], h[1])))
|
||||
bad.sort(key=lambda t: t[0])
|
||||
return bad
|
||||
|
||||
|
||||
def groups_of(net, pads, all_s, all_v):
|
||||
ps = [p for p in pads if p[7] == net]
|
||||
ss = [s for s in all_s if s[6] == net]
|
||||
vs = [v for v in all_v if v[2] == net]
|
||||
items = [('p', q) for q in ps] + [('s', s) for s in ss] + [('v', v) for v in vs]
|
||||
par = list(range(len(items)))
|
||||
|
||||
def f(i):
|
||||
while par[i] != i:
|
||||
par[i] = par[par[i]]
|
||||
i = par[i]
|
||||
return i
|
||||
|
||||
def touch_sp(s, p):
|
||||
n = max(2, int(math.hypot(s[3] - s[1], s[4] - s[2]) / 0.05))
|
||||
return s[0] in pad_layers(p) and min(
|
||||
pt_rect(s[1] + (s[3] - s[1]) * k / n, s[2] + (s[4] - s[2]) * k / n, prect(p)) for k in range(n + 1)) < s[5] / 2 + 0.01
|
||||
|
||||
for i in range(len(items)):
|
||||
for j in range(i + 1, len(items)):
|
||||
(ka, a), (kb, b) = items[i], items[j]
|
||||
if ka == 'p' and kb == 'p':
|
||||
hit = a[0] == b[0] and a[1] == b[1] # 同一只脚拆成的几块
|
||||
elif ka == 'v' and kb == 'v':
|
||||
hit = math.hypot(a[0] - b[0], a[1] - b[1]) < 0.61
|
||||
elif ka == 's' and kb == 's':
|
||||
hit = a[0] == b[0] and seg_seg(a[1:5], b[1:5]) < (a[5] + b[5]) / 2 + 0.01
|
||||
else:
|
||||
(k1, x), (k2, y) = sorted([(ka, a), (kb, b)], key=lambda z: z[0])
|
||||
if (k1, k2) == ('p', 's'):
|
||||
hit = touch_sp(y, x)
|
||||
elif (k1, k2) == ('p', 'v'):
|
||||
hit = pt_rect(y[0], y[1], prect(x)) < 0.3
|
||||
else: # ('s', 'v')
|
||||
hit = pt_seg(y[0], y[1], x[1], x[2], x[3], x[4]) < x[5] / 2 + 0.3
|
||||
if hit and f(i) != f(j):
|
||||
par[f(i)] = f(j)
|
||||
out = {}
|
||||
for i, (k, it) in enumerate(items):
|
||||
if k == 'p':
|
||||
out.setdefault(f(i), set()).add('%s.%s' % (it[0], it[1]))
|
||||
return [sorted(v) for v in out.values()]
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||
lim = int(sys.argv[sys.argv.index('--list') + 1]) if '--list' in sys.argv else 40
|
||||
args = [a for a in args if not a.isdigit()]
|
||||
res_file = args[0] if args else '_d16_route.out.json'
|
||||
res = json.load(io.open(os.path.join(HERE, res_file), encoding='utf-8'))
|
||||
brd = json.load(io.open(os.path.join(HERE, '_board_for_nets.json'), encoding='utf-8'))
|
||||
# ⚠ 通孔焊盘在 route_base 里拆出的 L1 障碍副本(脚号带 #t)不是真焊盘,这里读的是原始焊盘表,没有它们
|
||||
pads = [p for p in json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8'))
|
||||
if p[4] > 0.05 and p[5] > 0.05]
|
||||
npth = json.load(io.open(os.path.join(HERE, '_base_npth.json'), encoding='utf-8'))
|
||||
old_s = [tuple(s[:7]) for s in brd['segs']]
|
||||
old_v = [(v[0], v[1], v[2], v[3] if len(v) > 3 else 0.6) for v in brd['vias']]
|
||||
# d16_rip 的结果带「拆除清单」:这些旧铜下发时会先删掉,不算障碍、也不算连通
|
||||
rip_s = set(tuple(s[:7]) for s in res.get('ripped_segs', []))
|
||||
rip_v = set(tuple(v[:3]) for v in res.get('ripped_vias', []))
|
||||
if rip_s or rip_v:
|
||||
old_s = [s for s in old_s if s not in rip_s]
|
||||
old_v = [v for v in old_v if v[:3] not in rip_v]
|
||||
print(u'(按拆除清单去掉旧铜 %d 段 / %d 孔)' % (len(rip_s), len(rip_v)))
|
||||
new_s = [tuple(s[:7]) for s in res['segs']]
|
||||
new_v = [(v[0], v[1], v[2], 0.6) for v in res['vias']]
|
||||
bad = check_clearance(new_s, new_v, old_s, old_v, pads, npth)
|
||||
print(u'新增 %d 段 / %d 孔;净距 / 窗口问题 %d 条' % (len(new_s), len(new_v), len(bad)))
|
||||
for d, m in bad[:lim]:
|
||||
print(u' %6.3f %s' % (d, m))
|
||||
all_s, all_v = old_s + new_s, old_v + new_v
|
||||
print(u'连通(按铜,不含铺铜):')
|
||||
left = []
|
||||
cfg1 = [q[7] for q in pads if q[0] == 'U36' and str(q[1]) == '9'][0]
|
||||
for net in NETS:
|
||||
net = cfg1 if net == '#CFG1' else net
|
||||
g = groups_of(net, pads, all_s, all_v)
|
||||
if len(g) > 1:
|
||||
left.append(net)
|
||||
print(u' %-10s %d 块:%s' % (net, len(g), (u' | '.join(u','.join(x[:4]) for x in g))[:320]))
|
||||
else:
|
||||
print(u' %-10s 连通' % net)
|
||||
refs = set()
|
||||
for p in pads:
|
||||
if p[7] == 'GND' and p[6] != 12:
|
||||
refs.add(p[0])
|
||||
gnd_new = [p for p in pads if p[7] == 'GND' and p[6] == 2 and p[0] in set(json.load(
|
||||
io.open(os.path.join(HERE, '_base_place_live.json'), encoding='utf-8'))) and p[0] in res.get('pd_refs', [])]
|
||||
if gnd_new:
|
||||
# 按「位号.脚号」合并:一只脚常拆成几块焊盘(U37.1 = 主块 + 小块),接在任一块上都算接上
|
||||
byk = {}
|
||||
for p in gnd_new:
|
||||
byk.setdefault((p[0], str(p[1])), []).append(p)
|
||||
nov = [u'%s.%s' % k for k, ps in sorted(byk.items())
|
||||
if not any(any(v[2] == 'GND' and pt_rect(v[0], v[1], prect(p)) < 0.3 for v in all_v) or
|
||||
any(s[6] == 'GND' and s[0] == 2 and seg_rect(s[1:5], prect(p)) < s[5] / 2 + 0.01 for s in all_s)
|
||||
for p in ps)]
|
||||
print(u'新件 GND 贴片脚没接任何线 / 孔(只能靠铺铜):%s' % (u' '.join(nov) or u'无'))
|
||||
print(u'[OK] 可以下发' if not bad and not left else u'[X] 净距 %d 条 / 未连通 %s' % (len(bad), left))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户