Files
2026-10-15 01:59:40 +08:00

385 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""画原理图页的高层原语: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