sync: HP_BOWEI 2026-10-15

This commit is contained in:
Myfro9
2026-10-15 01:59:40 +08:00
parent 771acd4171
commit a0a3445bfd
21 changed files with 3042 additions and 16 deletions
+384
View File
@@ -0,0 +1,384 @@
"""画原理图页的高层原语: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