sync: HP_BOWEI 2026-10-15
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
"""写 .kicad_sch 的底层:符号提取、外框、旋转变换、页面拼装。
|
||||
|
||||
符号来源用注册制:KiCad 自带库按库名直接找;工程自己的库用 register_lib(name, path)
|
||||
登记(例如 register_lib("myboard", r"...\\lib\\myboard.kicad_sym"))。
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import math
|
||||
|
||||
from .env import kicad_symbol_dir
|
||||
|
||||
_LIBS = {} # 库名 -> .kicad_sym 路径(工程库)
|
||||
_cache = {}
|
||||
|
||||
|
||||
_FP_FILTER = {} # lib_id -> 覆盖后的 ki_fp_filters
|
||||
|
||||
|
||||
def set_fp_filter(lib_id, flt):
|
||||
"""覆盖某个符号的 ki_fp_filters(封装选择器的"Apply footprint filters"用它)。
|
||||
KiCad 通用连接器符号自带 Connector*:*_1x??_* 之类的过滤,工程自己的封装库
|
||||
(如从参考设计提取的 VH3.96-2P)会被它藏掉,看起来像"没有封装"。flt 用空格分隔多个模式。"""
|
||||
_FP_FILTER[lib_id] = flt
|
||||
|
||||
|
||||
def register_lib(name, path):
|
||||
_LIBS[name] = path
|
||||
_cache.pop(name, None)
|
||||
_bbox_cache.clear()
|
||||
|
||||
|
||||
def _read(libname):
|
||||
if libname in _cache:
|
||||
return _cache[libname]
|
||||
p = _LIBS.get(libname) or os.path.join(kicad_symbol_dir(), libname + ".kicad_sym")
|
||||
_cache[libname] = io.open(p, encoding="utf-8").read()
|
||||
return _cache[libname]
|
||||
|
||||
|
||||
def balanced(text, start):
|
||||
"""从 text[start] 的 '(' 起取配平的整段 s-expression(跳过字符串里的括号)。"""
|
||||
d, j = 0, start
|
||||
while j < len(text):
|
||||
c = text[j]
|
||||
if c == '"':
|
||||
j += 1
|
||||
while j < len(text) and not (text[j] == '"' and text[j - 1] != '\\'):
|
||||
j += 1
|
||||
elif c == '(':
|
||||
d += 1
|
||||
elif c == ')':
|
||||
d -= 1
|
||||
if d == 0:
|
||||
return text[start:j + 1]
|
||||
j += 1
|
||||
raise ValueError("unbalanced")
|
||||
|
||||
|
||||
def symbol_def(lib_id):
|
||||
"""取出 (symbol "Lib:Name" ...) 定义,名字改成 lib_id 形式供 lib_symbols 用。
|
||||
KiCad 自带库里的 (extends "X") 派生符号会被展开成完整定义。"""
|
||||
lib, name = lib_id.split(":", 1)
|
||||
t = _read(lib)
|
||||
i = t.find('(symbol "%s"' % name)
|
||||
if i < 0:
|
||||
raise KeyError(lib_id)
|
||||
blk = balanced(t, i)
|
||||
m = re.search(r'\(extends "([^"]+)"\)', blk)
|
||||
if m:
|
||||
# 派生符号:拿父符号的图形/引脚,属性用子符号的(属性块有嵌套括号,必须按配平取)
|
||||
base = balanced(t, t.find('(symbol "%s"' % m.group(1)))
|
||||
body = re.sub(r'\(symbol "%s_' % re.escape(m.group(1)), '(symbol "%s_' % name, base)
|
||||
body = body.replace('(symbol "%s"' % m.group(1), '(symbol "%s"' % name, 1)
|
||||
for pm in re.finditer(r'\(property "([^"]*)"', blk):
|
||||
pr = balanced(blk, pm.start())
|
||||
k = body.find('(property "%s"' % pm.group(1))
|
||||
if k >= 0:
|
||||
old = balanced(body, k)
|
||||
body = body[:k] + pr + body[k + len(old):]
|
||||
else: # 父符号没有的属性(如 ki_fp_filters)插在最后一个属性后
|
||||
k = body.rfind('(property "')
|
||||
old = balanced(body, k)
|
||||
body = body[:k + len(old)] + "\n\t\t" + pr + body[k + len(old):]
|
||||
blk = body
|
||||
blk = blk.replace('(symbol "%s"' % name, '(symbol "%s"' % lib_id, 1)
|
||||
if lib_id in _FP_FILTER:
|
||||
k = blk.find('(property "ki_fp_filters"')
|
||||
if k >= 0:
|
||||
old = balanced(blk, k)
|
||||
blk = blk[:k] + re.sub(r'\(property "ki_fp_filters" "[^"]*"',
|
||||
lambda _: '(property "ki_fp_filters" "%s"' % _FP_FILTER[lib_id],
|
||||
old, count=1) + blk[k + len(old):]
|
||||
return blk
|
||||
|
||||
|
||||
def uid(*parts):
|
||||
"""确定性 uuid:同样的输入每次生成同样的 uuid,重跑脚本不会让 git diff 满屏。"""
|
||||
h = hashlib.md5(("|".join(str(p) for p in parts)).encode()).hexdigest()
|
||||
return "%s-%s-%s-%s-%s" % (h[0:8], h[8:12], h[12:16], h[16:20], h[20:32])
|
||||
|
||||
|
||||
def _unit_blocks(blk, unit):
|
||||
"""取出属于某个 unit 的子符号。KiCad 把各 unit 共用图形放在 NAME_0_x,
|
||||
引脚放在 NAME_<unit>_x,两边都要,只取后者会丢掉整个本体图形(G-0019)。"""
|
||||
if unit is None:
|
||||
return blk
|
||||
subs = []
|
||||
for m in re.finditer(r'\(symbol "([^"]*_(\d+)_\d+)"', blk):
|
||||
if int(m.group(2)) in (0, unit):
|
||||
subs.append(balanced(blk, m.start()))
|
||||
return "\n".join(subs) if subs else blk
|
||||
|
||||
|
||||
def pin_positions(lib_id, unit=None):
|
||||
"""{pin_number: (dx, dy, rot)},相对符号原点;符号坐标 y 向上为正。"""
|
||||
blk = _unit_blocks(symbol_def(lib_id), unit)
|
||||
res = {}
|
||||
for m in re.finditer(r'\(pin\s+\S+\s+\S+\s*\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)', blk):
|
||||
x, y, rot = float(m.group(1)), float(m.group(2)), float(m.group(3))
|
||||
seg = blk[m.end():m.end() + 400]
|
||||
nm = re.search(r'\(number "([^"]*)"', seg)
|
||||
if nm:
|
||||
res[nm.group(1)] = (x, y, rot)
|
||||
return res
|
||||
|
||||
|
||||
GRID = 1.27
|
||||
|
||||
|
||||
def snap(v):
|
||||
"""吸附到 1.27mm 连接网格,否则 ERC 报 off-grid、线也连不上。"""
|
||||
return round(float(v) / GRID) * GRID
|
||||
|
||||
|
||||
_bbox_cache = {}
|
||||
|
||||
|
||||
def symbol_bbox(lib_id, unit=None):
|
||||
"""符号外框 (minx, miny, maxx, maxy),符号坐标系(y 向上),含引脚端点。"""
|
||||
key = (lib_id, unit)
|
||||
if key in _bbox_cache:
|
||||
return _bbox_cache[key]
|
||||
blk = _unit_blocks(symbol_def(lib_id), unit)
|
||||
xs, ys = [], []
|
||||
N = r'(-?\d+(?:\.\d+)?)'
|
||||
for m in re.finditer(r'\((rectangle|polyline|circle|arc|bezier)\b', blk):
|
||||
g = balanced(blk, m.start())
|
||||
for mm in re.finditer(r'\((?:start|end|mid|xy|center)\s+' + N + r'\s+' + N, g):
|
||||
xs.append(float(mm.group(1))); ys.append(float(mm.group(2)))
|
||||
cm = re.search(r'\(center\s+' + N + r'\s+' + N + r'\s*\)\s*\(radius\s+' + N, g)
|
||||
if cm:
|
||||
cx, cy, r = float(cm.group(1)), float(cm.group(2)), float(cm.group(3))
|
||||
xs += [cx - r, cx + r]; ys += [cy - r, cy + r]
|
||||
for m in re.finditer(r'\(pin\b', blk):
|
||||
g = balanced(blk, m.start())
|
||||
am = re.search(r'\(at\s+' + N + r'\s+' + N, g)
|
||||
if am:
|
||||
xs.append(float(am.group(1))); ys.append(float(am.group(2)))
|
||||
if not xs:
|
||||
xs, ys = [-2.54, 2.54], [-2.54, 2.54]
|
||||
bb = (min(xs), min(ys), max(xs), max(ys))
|
||||
_bbox_cache[key] = bb
|
||||
return bb
|
||||
|
||||
|
||||
def rot_pt(dx, dy, rot):
|
||||
a = math.radians(rot)
|
||||
c, s = math.cos(a), math.sin(a)
|
||||
return (dx * c - dy * s, dx * s + dy * c)
|
||||
|
||||
|
||||
def rot_bbox(bb, rot):
|
||||
xs, ys = [], []
|
||||
for x in (bb[0], bb[2]):
|
||||
for y in (bb[1], bb[3]):
|
||||
px, py = rot_pt(x, y, rot)
|
||||
xs.append(px); ys.append(py)
|
||||
return (min(xs), min(ys), max(xs), max(ys))
|
||||
|
||||
|
||||
def placed_pins(lib_id, unit=None, rot=0):
|
||||
"""{pin: (dx_sheet, dy_sheet, rot_sheet)}:已含旋转与 y 翻转,加到放置坐标上即图纸坐标。"""
|
||||
out = {}
|
||||
for k, (dx, dy, prot) in pin_positions(lib_id, unit).items():
|
||||
rx, ry = rot_pt(dx, dy, rot)
|
||||
out[k] = (rx, -ry, (prot + rot) % 360)
|
||||
return out
|
||||
|
||||
|
||||
def pin_dir(rot_sheet):
|
||||
"""引脚出线方向(图纸坐标,y 向下)。引脚 angle 指向本体内侧,出线取反向。"""
|
||||
a = math.radians(rot_sheet)
|
||||
return (-math.cos(a), math.sin(a))
|
||||
|
||||
|
||||
def esc(s):
|
||||
return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
|
||||
|
||||
class Sheet:
|
||||
"""一页 .kicad_sch 的内容。子图不写 sheet_instances(那是根图独有的)。"""
|
||||
|
||||
def __init__(self, title, project, paper="A3", rev="A", company="",
|
||||
sheet_uuid=None, root_uuid=None):
|
||||
# sheet_uuid 必须等于根图里该页 sheet 符号的 uuid,否则 KiCad 判为批注错误
|
||||
self.sheet_uuid = sheet_uuid or uid("root", title)
|
||||
self.root_uuid = root_uuid
|
||||
self.title, self.project, self.paper = title, project, paper
|
||||
self.rev, self.company = rev, company
|
||||
self.items, self.libs, self.n = [], {}, 0
|
||||
|
||||
def _need(self, lib_id):
|
||||
if lib_id not in self.libs:
|
||||
self.libs[lib_id] = symbol_def(lib_id)
|
||||
|
||||
def _path(self):
|
||||
return "/%s/%s" % (self.root_uuid, self.sheet_uuid) if self.root_uuid \
|
||||
else "/%s" % self.sheet_uuid
|
||||
|
||||
def comp(self, lib_id, ref, value, x, y, rot=0, unit=1, footprint="",
|
||||
hide_ref=False, hide_val=False, ref_at=None, val_at=None, dnp=False,
|
||||
fields=None, in_bom=True):
|
||||
"""放置元件。ref_at/val_at = (绝对x, 绝对y, 对齐),由调用方按外框算好。
|
||||
fields: 额外属性 {名字: 值}(如 MPN),一律隐藏。"""
|
||||
x, y = snap(x), snap(y)
|
||||
self._need(lib_id)
|
||||
bb = rot_bbox(symbol_bbox(lib_id, unit), rot)
|
||||
if ref_at is None:
|
||||
ref_at = (x, y - bb[3] - 2.032, "center")
|
||||
if val_at is None:
|
||||
val_at = (x, y - bb[1] + 2.032, "center")
|
||||
rxa, rya, rj = ref_at
|
||||
vxa, vya, vj = val_at
|
||||
# justify 只认 left/right/top/bottom/mirror;写 center 会让整页静默解析失败(G-0018)
|
||||
rj = "" if rj == "center" else " (justify %s)" % rj
|
||||
vj = "" if vj == "center" else " (justify %s)" % vj
|
||||
hidr = " hide" if hide_ref else ""
|
||||
hidv = " hide" if hide_val else ""
|
||||
# 90/270 放置时 KiCad 把元件角度叠到字段上,反向补偿;180 走镜像(G-0019)
|
||||
fa = {0: 0, 90: 270, 180: 0, 270: 90}[rot % 360]
|
||||
self.n += 1
|
||||
u = uid(self.title, ref, unit, self.n)
|
||||
extra = ""
|
||||
for k, v in (fields or {}).items():
|
||||
extra += (' (property "%s" "%s" (at %.2f %.2f 0)'
|
||||
' (effects (font (size 1.27 1.27)) hide))\n' % (esc(k), esc(v), x, y))
|
||||
self.items.append(
|
||||
' (symbol (lib_id "%s") (at %.2f %.2f %d) (unit %d)\n'
|
||||
' (exclude_from_sim no) (in_bom %s) (on_board yes) (dnp %s)\n'
|
||||
' (uuid "%s")\n'
|
||||
' (property "Reference" "%s" (at %.2f %.2f %d)'
|
||||
' (effects (font (size 1.27 1.27))%s%s))\n'
|
||||
' (property "Value" "%s" (at %.2f %.2f %d)'
|
||||
' (effects (font (size 1.27 1.27))%s%s))\n'
|
||||
' (property "Footprint" "%s" (at %.2f %.2f 0) (effects (font (size 1.27 1.27)) hide))\n'
|
||||
' (property "Datasheet" "" (at %.2f %.2f 0) (effects (font (size 1.27 1.27)) hide))\n'
|
||||
'%s'
|
||||
' (instances (project "%s" (path "%s" (reference "%s") (unit %d))))\n'
|
||||
' )'
|
||||
% (lib_id, x, y, rot, unit, ("yes" if in_bom else "no"), ("yes" if dnp else "no"), u,
|
||||
ref, rxa, rya, fa, rj, hidr,
|
||||
esc(value), vxa, vya, fa, vj, hidv,
|
||||
footprint, x, y, x, y, extra, self.project, self._path(), ref, unit))
|
||||
return (x, y)
|
||||
|
||||
def wire(self, x1, y1, x2, y2):
|
||||
x1, y1, x2, y2 = snap(x1), snap(y1), snap(x2), snap(y2)
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (wire (pts (xy %.2f %.2f) (xy %.2f %.2f))\n'
|
||||
' (stroke (width 0) (type default)) (uuid "%s"))'
|
||||
% (x1, y1, x2, y2, uid(self.title, "w", x1, y1, x2, y2, self.n)))
|
||||
|
||||
# 标签文字角度只有 0/90,朝向由 justify 决定;写 180/270 会反向压在元件上(G-0019)
|
||||
GLROT = {0: (0, "left"), 180: (0, "right"), 90: (90, "left"), 270: (90, "right")}
|
||||
|
||||
def glabel(self, name, x, y, direction=0, shape="bidirectional"):
|
||||
"""direction: 标签体伸出方向,0=右 90=上 180=左 270=下。"""
|
||||
x, y = snap(x), snap(y)
|
||||
ang, just = self.GLROT[direction % 360]
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (global_label "%s" (shape %s) (at %.2f %.2f %d) (fields_autoplaced yes)\n'
|
||||
' (effects (font (size 1.27 1.27)) (justify %s)) (uuid "%s"))'
|
||||
% (name, shape, x, y, ang, just, uid(self.title, "gl", name, x, y, self.n)))
|
||||
|
||||
def text(self, s, x, y, size=1.6):
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (text "%s" (at %.2f %.2f 0)\n'
|
||||
' (effects (font (size %.2f %.2f)) (justify left bottom)) (uuid "%s"))'
|
||||
% (esc(s.replace('"', "'")), x, y, size, size, uid(self.title, "t", s, x, y, self.n)))
|
||||
|
||||
def rect(self, x0, y0, x1, y1, dash=True):
|
||||
"""虚线框:圈出一个子电路(例如"留位不贴"的一组件)。"""
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (rectangle (start %.2f %.2f) (end %.2f %.2f)\n'
|
||||
' (stroke (width 0.2) (type %s)) (fill (type none)) (uuid "%s"))'
|
||||
% (x0, y0, x1, y1, "dash" if dash else "default",
|
||||
uid(self.title, "rect", x0, y0, self.n)))
|
||||
|
||||
def junction(self, x, y):
|
||||
"""丁字接点:一条线的端点落在另一条线中段上,必须放 junction 才导通(G-0036)。"""
|
||||
x, y = snap(x), snap(y)
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (junction (at %.2f %.2f) (diameter 0) (color 0 0 0 0)'
|
||||
' (uuid "%s"))' % (x, y, uid(self.title, "j", x, y, self.n)))
|
||||
|
||||
def nc(self, x, y):
|
||||
x, y = snap(x), snap(y)
|
||||
self.n += 1
|
||||
self.items.append(' (no_connect (at %.2f %.2f) (uuid "%s"))'
|
||||
% (x, y, uid(self.title, "nc", x, y, self.n)))
|
||||
|
||||
def raw(self, sexpr):
|
||||
self.items.append(sexpr)
|
||||
|
||||
def dump(self, path, generator="kicad_flow_schgen"):
|
||||
out = ['(kicad_sch (version 20231120) (generator "%s") (generator_version "8.0")' % generator,
|
||||
' (uuid "%s")' % uid("sheetfile", self.title),
|
||||
' (paper "%s")' % self.paper,
|
||||
' (title_block (title "%s") (rev "%s") (company "%s"))'
|
||||
% (esc(self.title), self.rev, esc(self.company)),
|
||||
' (lib_symbols']
|
||||
for k in sorted(self.libs):
|
||||
out.append(" " + self.libs[k].replace("\n", "\n "))
|
||||
out.append(' )')
|
||||
out.extend(self.items)
|
||||
out.append(')')
|
||||
io.open(path, "w", encoding="utf-8").write("\n".join(out) + "\n")
|
||||
return path
|
||||
Reference in New Issue
Block a user