文件

256 行
12 KiB
Python

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