"""画原理图页的高层原语:Sheet 负责写文件,Canvas 负责记账做重叠/越界检查,两者成对更新。 用法(一个工程一个 Project,多页共享位号登记表与封装解析): from schgen import Project prj = Project("MyBoard", out_dir=r"E:\\...\\MyBoard", fp_resolver=lambda ref, val: FPMAP[val]) pg = prj.page("02 Power", "02_Power.kicad_sch", paper="A3") x, y, pp = pg.comp("Device:R", "R1", "10K", 100, 80) ... pg.finish() # 有重叠/越界 -> 打印并返回 False,不写文件 画法约定(G-0035 / G-0037):两脚无源件必须画进它服务的电路里—— 串联件用 inline()/fan_series(),退耦/上下拉用 shunt()/run_taps() 挂在母线上。 不要 two_pin() 摆在空地两端各挂一个标签(只留给确实孤立的跳线位)。 """ import os from .kicad_lib import (Sheet, placed_pins, pin_dir, symbol_bbox, rot_bbox, uid, snap) from .layout import Canvas, LBL_PAD, strw GND = "GND" DEFAULT_PWRSYM = {"GND": "power:GND", "+3V3": "power:+3V3", "+5V": "power:+5V", "+12V": "power:+12V", "+1V2": "power:+1V2", "+1V8": "power:+1V8", "+2V5": "power:+2V5", "+3V3A": "power:+3V3A"} R, C, L = "Device:R", "Device:C", "Device:L" class Project: def __init__(self, name, out_dir, fp_resolver=None, pwrsym=None, company="", rev="A", props_resolver=None): """fp_resolver(ref, val) -> "库:封装";props_resolver(ref, val) -> {"MPN": ..., ...} (额外隐藏属性,BOM 用)。""" self.name, self.out_dir = name, out_dir self.fp_resolver = fp_resolver or (lambda ref, val: "") self.props_resolver = props_resolver or (lambda ref, val: {}) self.pwrsym = dict(DEFAULT_PWRSYM, **(pwrsym or {})) self.company, self.rev = company, rev self.refs = {} # 位号登记表:KiCad 对重名位号静默合并(G-0028) self.npwr = 0 self.pages = [] def page(self, title, fname, paper="A3"): p = Page(self, title, fname, paper) self.pages.append(p) return p def footprint(self, ref, val): if ref.startswith("#"): return "" fp = self.fp_resolver(ref, val) if fp is None: raise KeyError("没有为 %s (%s) 指定封装" % (ref, val)) return fp def pwr_rot(lib, ux, uy): """选放置角度,让电源符号顺着出线方向伸出去,不倒压回走线。""" bb = symbol_bbox(lib) for rot in (0, 90, 180, 270): b = rot_bbox(bb, rot) sx = (b[0] + b[2]) / 2.0 sy = -(b[1] + b[3]) / 2.0 if ux * sx + uy * sy > 0.05: return rot return 0 class Page: def __init__(self, prj, title, fname, paper="A3"): self.prj = prj self.sh = Sheet(title, prj.name, paper=paper, rev=prj.rev, company=prj.company, sheet_uuid=uid("sh", fname)) self.cv = Canvas(paper=paper, name=fname) self.fname = fname self.frames = [] # ---------- 基本元素 ---------- def text(self, s, x, y, size=1.6): """多行用 \\n;每行都登记占位。""" self.sh.text(s, x, y, size) lines = s.split("\n") for k, ln in enumerate(lines): yy = y - (len(lines) - 1 - k) * size * 1.35 self.cv.add_text(x, yy, ln, "left", size, what="text:" + ln[:20]) def wire(self, x1, y1, x2, y2): x1, y1, x2, y2 = snap(x1), snap(y1), snap(x2), snap(y2) self.sh.wire(x1, y1, x2, y2) self.cv.add_wire(x1, y1, x2, y2) @staticmethod def _lrot(ux, uy): if ux > 0.5: return 0 if ux < -0.5: return 180 return 270 if uy > 0 else 90 def label(self, name, x, y, ux, uy): """在 (x,y) 放全局标签,标签体朝 (ux,uy) 伸出。""" lrot = self._lrot(ux, uy) self.sh.glabel(name, x, y, lrot) self.cv.add_label(x, y, name, lrot, what="lbl:" + name) def power(self, net, x, y, ux, uy): """电源符号:藏 #PWRxxxx,但网名要显示。""" lib = self.prj.pwrsym[net] rot = pwr_rot(lib, ux, uy) bb = rot_bbox(symbol_bbox(lib), rot) self.prj.npwr += 1 ref = "#PWR%04d" % self.prj.npwr lrot = self._lrot(ux, uy) if lrot == 0: vx, vy, vj = x + bb[2] + 1.0, y, "left" elif lrot == 180: vx, vy, vj = x + bb[0] - 1.0, y, "right" elif lrot == 90: vx, vy, vj = x, y - bb[3] - 1.6, "center" else: vx, vy, vj = x, y - bb[1] + 1.6, "center" self.sh.comp(lib, ref, net, x, y, rot=rot, hide_ref=True, ref_at=(x, y, "left"), val_at=(vx, vy, vj)) self.cv.add_sym(x, y, bb, "sym:" + ref) self.cv.add_text(vx, vy, net, vj, what="ref:" + ref + "=") def nc(self, x, y): self.sh.nc(x, y) self.cv.add(x - 1.0, y - 1.0, x + 1.0, y + 1.0, "nc:NC") def frame(self, x0, y0, x1, y1, title=None, size=1.4): """虚线框圈出一组件(如"留位不贴")。框线不参与重叠检查,标题文字参与。""" self.sh.rect(x0, y0, x1, y1) if title: self.text(title, x0 + 1.5, y0 - 1.0, size) # ---------- 元件 ---------- def comp(self, lib, ref, val, x, y, rot=0, unit=1, fields="above", show=True, dnp=False, props=None, in_bom=True): """fields: 'above' 位号上/数值下;'left' 叠在左侧;'right' 叠在右侧; 'top' 都叠在上方;'none' 不显示;("xy", rx, ry, vx, vy) 手动指定(大芯片常用)。 返回 (x, y, 已旋转的引脚表)。""" x, y = snap(x), snap(y) key = (ref, unit) prev = self.prj.refs.get(ref) if key in self.prj.refs or (prev and prev[1] != val): o = self.prj.refs.get(key, prev) raise SystemExit("位号撞车 %s:%s 的 %s 与 %s 的 %s" % (ref, self.fname, val, o[0], o[1])) self.prj.refs[key] = self.prj.refs[ref] = (self.fname, val) bb = rot_bbox(symbol_bbox(lib, unit), rot) fp = self.prj.footprint(ref, val) if not ref.startswith("#"): props = dict(self.prj.props_resolver(ref, val) or {}, **(props or {})) dval = val # DNP 件 KiCad 只画灰色 + 红叉,数值字段原样,不加 "DNP" 字样 if not show or fields == "none": self.sh.comp(lib, ref, val, x, y, rot=rot, unit=unit, hide_ref=True, hide_val=True, dnp=dnp, footprint=fp, fields=props, in_bom=in_bom) else: if isinstance(fields, tuple): # ("xy", 位号x, 位号y, 数值x, 数值y),左对齐 ra, va = (fields[1], fields[2], "left"), (fields[3], fields[4], "left") elif fields == "left": fx = x + bb[0] - 1.6 ra, va = (fx, y - 1.0, "right"), (fx, y + 2.0, "right") elif fields == "right": fx = x + bb[2] + 1.6 ra, va = (fx, y - 1.0, "left"), (fx, y + 2.0, "left") elif fields == "top": ra = (x, y - bb[3] - 4.1, "center") va = (x, y - bb[3] - 2.032, "center") else: ra = (x, y - bb[3] - 2.032, "center") va = (x, y - bb[1] + 2.032, "center") self.cv.add_text(ra[0], ra[1], ref, ra[2], what="ref:" + ref) self.cv.add_text(va[0], va[1], dval, va[2], what="ref:" + ref + "=") self.sh.comp(lib, ref, val, x, y, rot=rot, unit=unit, ref_at=ra, val_at=va, dnp=dnp, footprint=fp, fields=props, in_bom=in_bom) self.cv.add_sym(x, y, bb, "sym:" + ref) pp = placed_pins(lib, unit, rot) return x, y, pp def pin_xy(self, cx, cy, pp, pin): return cx + pp[pin][0], cy + pp[pin][1] def stub(self, cx, cy, pp, pin, net, length=6.35, stagger=False): """从引脚按其方向拉线,末端接全局标签或电源符号。 stagger:相邻行电源引线一长一短错开,免得电源符号肩并肩顶在一起。""" if pin not in pp: raise KeyError("引脚 %s 不存在" % pin) dx, dy, prot = pp[pin] px, py = cx + dx, cy + dy ux, uy = pin_dir(prot) if stagger and net in self.prj.pwrsym: row = int(round((dy if abs(ux) > 0.5 else dx) / 2.54)) if row % 2: length += 7.62 ex, ey = snap(px + ux * length), snap(py + uy * length) self.wire(px, py, ex, ey) self.term(net, ex, ey, ux, uy) return ex, ey def nc_pin(self, cx, cy, pp, pin): px, py = self.pin_xy(cx, cy, pp, pin) self.nc(px, py) def two_pin(self, lib, ref, val, x, y, na, nb, rot=0, stub=6.35, dnp=False): """两脚件两端各挂标签。只用于确实孤立的件;外围件请用 inline/shunt。""" pp = placed_pins(lib, 1, rot) d1, d2 = pp["1"], pp["2"] vertical = abs(d1[1] - d2[1]) > abs(d1[0] - d2[0]) x, y, pp = self.comp(lib, ref, val, x, y, rot=rot, dnp=dnp, fields="left" if vertical else "above") for pin, net in (("1", na), ("2", nb)): self.stub(x, y, pp, pin, net, stub) return x, y # ---------- 子电路原语 ---------- def _ends(self, lib, rot, ux, uy): """沿 (ux,uy) 方向返回 (近端脚号, 远端脚号)。""" p = placed_pins(lib, 1, rot) a, b = sorted(p.keys())[:2] ja = p[a][0] * ux + p[a][1] * uy jb = p[b][0] * ux + p[b][1] * uy return (a, b) if ja < jb else (b, a) def dot(self, x, y): x, y = snap(x), snap(y) self.sh.junction(x, y) self.cv.add(x - 0.6, y - 0.6, x + 0.6, y + 0.6, "jct") def term(self, net, x, y, ux, uy): """线的末端:标准轨用电源符号,其余用全局标签;net=None 表示什么都不接。""" if net is None: return if net in self.prj.pwrsym: self.power(net, snap(x), snap(y), ux, uy) else: self.label(net, snap(x), snap(y), ux, uy) def seg(self, x1, y1, x2, y2): if abs(x1 - x2) > 0.01 or abs(y1 - y2) > 0.01: self.wire(snap(x1), snap(y1), snap(x2), snap(y2)) def path(self, *pts): """折线:path((x0,y0), (x1,y1), ...)""" for a, b in zip(pts, pts[1:]): self.seg(a[0], a[1], b[0], b[1]) def fan_series(self, cx, cy, pp, items, side=1, dist=40.0, pitch=12.7, tail=6.35, ch0=18.0, chstep=3.81, taps=None): """一组引脚上的串联件:引脚 -L形走线-> 元件 -> 标签,元件排成一列。 items: [(引脚号, lib, 位号, 数值, 对外网名), ...];side +1 右 / -1 左。 taps: {引脚号: 抽头点相对 cx 的偏移},在串阻外侧留接点挂上下拉。""" ys = [cy + pp[p][1] for p, _, _, _, _ in items] y0 = sum(ys) / len(ys) - pitch * (len(items) - 1) / 2.0 ex = snap(cx + side * dist) out = {} near, far = self._ends(items[0][1], 90, side, 0) for k, (pin, lib, ref, val, net) in enumerate(items): px, py = cx + pp[pin][0], cy + pp[pin][1] x, y, epp = self.comp(lib, ref, val, ex, snap(y0 + k * pitch), rot=90, fields="above") nx, ny = x + epp[near][0], y + epp[near][1] mx = snap(cx + side * (ch0 + k * chstep)) self.seg(px, py, mx, py) self.seg(mx, py, mx, ny) self.seg(mx, ny, nx, ny) fx, fy = x + epp[far][0], y + epp[far][1] toff = (taps or {}).get(pin) if toff is not None: hx = snap(cx + side * toff) self.seg(fx, fy, hx, fy) self.dot(hx, fy) fx, fy = hx, fy out[pin] = (hx, fy) tx = snap(fx + side * tail) self.seg(fx, fy, tx, fy) self.term(net, tx, fy, side, 0) return out def shunt(self, bx, by, lib, ref, val, net, down=True, tail=6.35, dnp=False, drop=9.0, fields="left", rot=0): """从母线上一点往下(或往上)挂一个件,末端接电源符号或标签。 退耦电容、上下拉电阻都用它。返回 (件中心x, y, 远端x, 远端y)。""" sgn = 1 if down else -1 near, far = self._ends(lib, rot, 0, sgn) x, y, epp = self.comp(lib, ref, val, snap(bx), snap(by + sgn * drop), rot=rot, fields=fields, dnp=dnp) nx, ny = x + epp[near][0], y + epp[near][1] fx, fy = x + epp[far][0], y + epp[far][1] self.seg(bx, by, nx, ny) ty = snap(fy + sgn * tail) if tail > 0: self.seg(fx, fy, fx, ty) self.term(net, fx, ty, 0, sgn) return x, y, fx, fy def gnd_bus(self, cx, cy, pp, pins, side=1, dist=13.0, drop=9.0, net=GND): """一侧若干地脚并到一条竖直母线上,底端只放一个地符号。""" ys = [] bx = snap(cx + side * dist) for p in pins: px, py = cx + pp[p][0], cy + pp[p][1] self.seg(px, py, bx, py) ys.append(snap(py)) y0, y1 = min(ys), max(ys) self.seg(bx, y0, bx, snap(y1 + drop)) for yy in ys: if abs(yy - y0) > 0.01: self.dot(bx, yy) self.power(net, bx, snap(y1 + drop), 0, 1) def run_taps(self, px, py, ux, stops, net=None, end=8.0): """从 (px,py) 沿 ±x 拉线,沿途在指定 x 上打抽头挂件,末端出标签。 stops: [(抽头 x, 'up'|'dn', lib, 位号, 数值, 另一端的网[, dnp]), ...]""" cur = px for k, st in enumerate(stops): hx = snap(st[0]) self.seg(cur, py, hx, py) self.shunt(hx, py, st[2], st[3], st[4], st[5], down=(st[1] == "dn"), dnp=(st[6] if len(st) > 6 else False)) if k < len(stops) - 1 or net is not None: self.dot(hx, py) cur = hx if net is not None: tx = snap(cur + ux * end) self.seg(cur, py, tx, py) self.term(net, tx, py, ux, 0) return cur def gnd_row(self, cx, cy, pp, pins, drop=6.0, tail=5.0, at=0, net=GND): """一排朝下的地脚各自下拉一小段并到横母线,母线上只落一个地符号。""" xs = [] for p in pins: px, py = cx + pp[p][0], cy + pp[p][1] self.seg(px, py, px, snap(py + drop)) xs.append(snap(px)) yb = snap(cy + pp[pins[0]][1] + drop) self.seg(min(xs), yb, max(xs), yb) gx = xs[at] self.seg(gx, yb, gx, snap(yb + tail)) self.power(net, gx, snap(yb + tail), 0, 1) for k, xx in enumerate(xs): interior = 0.01 < abs(xx - min(xs)) and abs(xx - max(xs)) > 0.01 if interior or k == at: self.dot(xx, yb) def inline(self, x0, y0, lib, ref, val, side=1, length=12.7, dnp=False, fields="above", rot=90): """在水平线上串一个件:从 (x0,y0) 出发,返回线另一端坐标。 二极管这类本身横放的符号传 rot=0/180 来定正反向。""" near, far = self._ends(lib, rot, side, 0) cx = snap(x0 + side * length / 2.0) x, y, epp = self.comp(lib, ref, val, cx, snap(y0), rot=rot, fields=fields, dnp=dnp) nx, ny = x + epp[near][0], y + epp[near][1] fx, fy = x + epp[far][0], y + epp[far][1] self.seg(x0, y0, nx, ny) return fx, fy def vline(self, x0, y0, lib, ref, val, down=True, length=12.7, dnp=False, fields="left", rot=0): """在竖直线上串一个件:从 (x0,y0) 出发,返回线另一端坐标。""" sgn = 1 if down else -1 near, far = self._ends(lib, rot, 0, sgn) cy = snap(y0 + sgn * length / 2.0) x, y, epp = self.comp(lib, ref, val, snap(x0), cy, rot=rot, fields=fields, dnp=dnp) nx, ny = x + epp[near][0], y + epp[near][1] fx, fy = x + epp[far][0], y + epp[far][1] self.seg(x0, y0, nx, ny) return fx, fy def finish(self, force=False): out, ov = self.cv.report() if out or ov: print(" !! %s 越界 %d 处, 重叠 %d 处" % (self.fname, len(out), len(ov))) for b in out[:15]: print(" 越界 %-28s (%.1f,%.1f)-(%.1f,%.1f) 图框 (%.0f,%.0f)-(%.0f,%.0f)" % (b[4], b[0], b[1], b[2], b[3], self.cv.x0, self.cv.y0, self.cv.x1, self.cv.y1)) for o in ov[:25]: print(" 重叠 %-28s x %-28s @(%.1f,%.1f)" % o) if not force: return False self.sh.dump(os.path.join(self.prj.out_dir, self.fname)) print(" OK %-26s 元素 %4d 个" % (self.fname, len(self.cv.boxes))) return True