#!/usr/bin/env python3 """对账 2:从几何重建网表,和 EasyEDA 权威网表逐网络比对。 为什么必须做这一步:KiCad 的连接性同样是从几何算出来的。只要「按同样坐标摆符号 + 画同样导线」重建出的网表能和 EasyEDA 编译结果对上,生成的 .kicad_sch 就能对上; 对不上就说明我对几何的理解有洞,必须先补。 规则(都来自 easyeda-sch-geometry skill 的实测): - getState_Line() 是**端点对**:每 4 个数一段,不是折线路径。当折线解析会凭空造斜线。 - 坐标单位 0.01 inch,元件都吸附在 5 的倍数上;TOL=0.6 足够宽松又不会误连。 - netflag / netport 各带一个引脚,其 Net 字段就是网络名;**跨页靠同名 netport/netflag 合并**。 """ import json, os, sys, collections sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from k_netlist import parse_pads ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "eda_src") TOL = 0.6 class DSU: def __init__(self): self.p = {} def find(self, x): self.p.setdefault(x, x) while self.p[x] != x: self.p[x] = self.p[self.p[x]]; x = self.p[x] return x def union(self, a, b): ra, rb = self.find(a), self.find(b) if ra != rb: self.p[ra] = rb def key(x, y): """吸附到 0.5 网格再取整 —— 坐标里有 539.9999999999999 这种浮点毛刺。""" return (round(x * 2) / 2, round(y * 2) / 2) def segments(line): """端点对 -> [((x1,y1),(x2,y2)), ...]""" out = [] for i in range(0, len(line) - 3, 4): a, b = key(line[i], line[i + 1]), key(line[i + 2], line[i + 3]) if a != b: # 零长度段丢掉(skill §8a 的坑,连接性上无意义) out.append((a, b)) return out def build(board): tree = json.load(open(os.path.join(ROOT, "_tree.json"), encoding="utf-8")) pages = next(x for x in tree["boards"] if x["board"] == board)["pages"] dsu = DSU() pinat = collections.defaultdict(list) # 坐标 -> ["R1.2", ...] named = collections.defaultdict(list) # 网络名 -> [坐标, ...] for p in pages: g = json.load(open(os.path.join(ROOT, board, p["name"] + ".geom.json"), encoding="utf-8")) pfx = p["name"] for w in (g["prims"]["Wire"] or []): for a, b in segments(w.get("Line") or []): dsu.union((pfx, a), (pfx, b)) for c in (g["prims"]["Component"] or []): t = c.get("ComponentType") if t == "sheet": continue for q in (c.get("__pins") or []): k = (pfx, key(q["X"], q["Y"])) dsu.find(k) if t == "part": pinat[k].append(f'{c["Designator"]}.{q["PinNumber"]}') else: # netflag / netport n = (c.get("Net") or "").strip() if n: named[n].append(k) # 同名 netflag/netport 跨页合并 for n, ks in named.items(): for k in ks[1:]: dsu.union(ks[0], k) nets = collections.defaultdict(set) for k, refs in pinat.items(): nets[dsu.find(k)].update(refs) label = {} for n, ks in named.items(): label.setdefault(dsu.find(ks[0]), n) # 单引脚的不算网络 —— 那是未连接引脚,EasyEDA 网表也不收录(KiCad 侧要靠 NC 标记处理) out, nc = {}, [] for i, (r, v) in enumerate(nets.items()): if len(v) >= 2: out[label.get(r, f"@{i}")] = sorted(v) elif v: nc.extend(v) return out, sorted(nc) def compare(board): mine, nc = build(board) base = parse_pads(os.path.join(ROOT, board, "netlist.asc"))["nets"] # 按引脚集合配对,网络名不参与(EasyEDA 的 $1N236 是它自己编的) mset = {frozenset(v): k for k, v in mine.items()} bset = {frozenset(v): k for k, v in base.items()} only_b = [bset[s] for s in bset.keys() - mset.keys()] only_m = [mset[s] for s in mset.keys() - bset.keys()] print(f"[{board}] 重建 {len(mine)} 网 / 基准 {len(base)} 网;引脚集合完全一致的 {len(mset.keys() & bset.keys())}") if not only_b and not only_m: print(" ✔ 网表逐网络完全一致") # 命名一致性另报(不影响连接性) diff = [(bset[s], mset[s]) for s in bset.keys() & mset.keys() if not mset[s].startswith("@") and bset[s] != mset[s] and not bset[s].startswith("$")] if diff: print(f" · 网络名不同但连接相同 {len(diff)}: {diff[:5]}") print(f" · 未连接引脚 {len(nc)} 个(EasyEDA 网表不收录,KiCad 侧需打 NC): {nc[:6]}{' …' if len(nc)>6 else ''}") return True for nm in sorted(only_b)[:8]: print(f" ⚠ 基准有、重建没有: {nm} = {base[nm]}") for nm in sorted(only_m)[:8]: print(f" ⚠ 重建有、基准没有: {nm} = {mine[nm]}") print(f" (只在基准 {len(only_b)},只在重建 {len(only_m)})") return False if __name__ == "__main__": ok = all([compare(b) for b in (sys.argv[1:] or ["Board1", "Board2"])]) sys.exit(0 if ok else 1)