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

336 lines
14 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.
"""写 .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