228 行
11 KiB
Python
228 行
11 KiB
Python
#!/usr/bin/env python3
|
||
"""生成 KiCad 符号库 kicad/lib/PhoneRail.kicad_sym。
|
||
|
||
来源:EasyEDA 工程包里的 .esym(页面源码里没有符号图形,必须用工程包)。
|
||
|
||
坐标:EasyEDA 符号和 KiCad .kicad_sym **都是 Y 向上**,所以图形不翻 Y,只做单位换算
|
||
(1 单位 = 0.01 inch = 0.254 mm)。引脚 (x,y) 两边都是「连接点」,rotation 两边都是
|
||
「从连接点指向本体」的方向,可以直接映射 —— 已用 LMR51430YDDCR 实测核对。
|
||
|
||
产出两类符号:
|
||
· 68 个元件符号(part 引用的)
|
||
· 4 个电源符号(netflag 的 GND / +12V / +3V3 / +3V3_HUB),带 (power) 标记,
|
||
KiCad 靠它做电源网络的全局连接
|
||
"""
|
||
import json, os, re, sys, collections
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import k_esym
|
||
from k_esym import MM, PIN_TYPE
|
||
|
||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||
SRC = os.path.join(ROOT, "eda_src")
|
||
SYMDIR = os.path.join(SRC, "epro", "SYMBOL")
|
||
OUTDIR = os.path.join(os.path.dirname(ROOT), "kicad", "lib")
|
||
LIBNAME = "PhoneRail"
|
||
|
||
FONT = "(effects (font (size 1.27 1.27)))"
|
||
FONT_HIDE = "(effects (font (size 1.27 1.27)) (hide yes))"
|
||
|
||
|
||
def q(s):
|
||
"""S-expression 字符串转义"""
|
||
return '"' + str(s if s is not None else "").replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||
|
||
|
||
def n(v):
|
||
"""坐标数字:EasyEDA 单位 -> mm,去掉浮点毛刺"""
|
||
x = round(float(v) * MM, 4)
|
||
return f"{x:.4f}".rstrip("0").rstrip(".") or "0"
|
||
|
||
|
||
def safe(name):
|
||
"""KiCad 符号名不能带 : / 换行"""
|
||
return re.sub(r'[:/\r\n\t]', "_", (name or "UNNAMED")).strip()
|
||
|
||
|
||
def shapes_sexp(sym, ind):
|
||
out = []
|
||
for s in sym["shapes"]:
|
||
if s["k"] == "rect":
|
||
out.append(f'{ind}(rectangle (start {n(s["x1"])} {n(s["y1"])}) (end {n(s["x2"])} {n(s["y2"])})\n'
|
||
f'{ind}\t(stroke (width 0) (type default)) (fill (type background))\n{ind})')
|
||
elif s["k"] == "poly":
|
||
pts = " ".join(f"(xy {n(x)} {n(y)})" for x, y in s["pts"])
|
||
fill = "background" if s["closed"] else "none"
|
||
out.append(f'{ind}(polyline (pts {pts})\n'
|
||
f'{ind}\t(stroke (width 0) (type default)) (fill (type {fill}))\n{ind})')
|
||
elif s["k"] == "circle":
|
||
out.append(f'{ind}(circle (center {n(s["cx"])} {n(s["cy"])}) (radius {n(s["r"])})\n'
|
||
f'{ind}\t(stroke (width 0) (type default)) (fill (type none))\n{ind})')
|
||
elif s["k"] == "arc":
|
||
out.append(f'{ind}(arc (start {n(s["sx"])} {n(s["sy"])}) (mid {n(s["mx"])} {n(s["my"])}) '
|
||
f'(end {n(s["ex"])} {n(s["ey"])})\n'
|
||
f'{ind}\t(stroke (width 0) (type default)) (fill (type none))\n{ind})')
|
||
for t in sym["texts"]:
|
||
out.append(f'{ind}(text {q(t["s"])} (at {n(t["x"])} {n(t["y"])} {int(t.get("rot") or 0)}) {FONT})')
|
||
return out
|
||
|
||
|
||
def pins_sexp(sym, ind, hide_names=False):
|
||
out = []
|
||
for p in sym["pins"]:
|
||
et = PIN_TYPE.get(p["type"], "unspecified")
|
||
nm = p["name"] if p["name"] is not None else "~"
|
||
nf = FONT_HIDE if (hide_names or not p["nameVisible"]) else FONT
|
||
numf = FONT_HIDE if not p["numberVisible"] else FONT
|
||
out.append(
|
||
f'{ind}(pin {et} line (at {n(p["x"])} {n(p["y"])} {int(p["rot"]) % 360}) (length {n(p["len"])})\n'
|
||
f'{ind}\t(name {q(nm)} {nf})\n'
|
||
f'{ind}\t(number {q(p["number"] if p["number"] is not None else "")} {numf})\n{ind})')
|
||
return out
|
||
|
||
|
||
def symbol_sexp(kname, sym, ref, value, power=False, descr=""):
|
||
I = "\t"
|
||
L = [f'{I}(symbol {q(kname)}']
|
||
if power:
|
||
L.append(f'{I*2}(power)')
|
||
L.append(f'{I*2}(pin_names (offset 0) hide)')
|
||
else:
|
||
L.append(f'{I*2}(pin_names (offset 0.254))')
|
||
L += [f'{I*2}(exclude_from_sim no)', f'{I*2}(in_bom {"no" if power else "yes"})',
|
||
f'{I*2}(on_board {"no" if power else "yes"})']
|
||
bb = sym.get("bbox") or [-10, -10, 10, 10]
|
||
top = n(max(bb[1], bb[3]) + 8)
|
||
bot = n(min(bb[1], bb[3]) - 8)
|
||
L.append(f'{I*2}(property "Reference" {q(ref)} (at 0 {top} 0) {FONT})')
|
||
L.append(f'{I*2}(property "Value" {q(value)} (at 0 {bot} 0) {FONT})')
|
||
L.append(f'{I*2}(property "Footprint" "" (at 0 0 0) {FONT_HIDE})')
|
||
L.append(f'{I*2}(property "Datasheet" "" (at 0 0 0) {FONT_HIDE})')
|
||
L.append(f'{I*2}(property "Description" {q(descr)} (at 0 0 0) {FONT_HIDE})')
|
||
g = shapes_sexp(sym, I * 3)
|
||
L.append(f'{I*2}(symbol {q(kname + "_0_1")}')
|
||
L += g
|
||
L.append(f'{I*2})')
|
||
L.append(f'{I*2}(symbol {q(kname + "_1_1")}')
|
||
L += pins_sexp(sym, I * 3, hide_names=power)
|
||
L.append(f'{I*2})')
|
||
L.append(f'{I})')
|
||
return "\n".join(L)
|
||
|
||
|
||
def mirror_sym(sym):
|
||
"""沿 Y 轴镜像一个符号(X 取反)。
|
||
|
||
为什么不用 KiCad 的 (mirror y):EasyEDA 的镜像是「先旋转再翻 X」,而 KiCad 的
|
||
mirror 与旋转的复合次序不同,2026-09-07 实测直接用 (mirror y) 会让 U24~U28
|
||
的引脚落到镜像位置(对账报 U24.3 变成 U24.6)。生成显式镜像符号后,配合
|
||
角度取 (360-θ) 就能精确复现,且不依赖对 KiCad 内部次序的猜测。
|
||
"""
|
||
out = {"name": sym["name"], "bbox": None, "shapes": [], "texts": [], "pins": [], "skipped": []}
|
||
if sym.get("bbox"):
|
||
b = sym["bbox"]; out["bbox"] = [-b[2], b[1], -b[0], b[3]]
|
||
for sh in sym["shapes"]:
|
||
k = sh["k"]
|
||
if k == "rect":
|
||
out["shapes"].append({"k": "rect", "x1": -sh["x1"], "y1": sh["y1"],
|
||
"x2": -sh["x2"], "y2": sh["y2"]})
|
||
elif k == "poly":
|
||
out["shapes"].append({"k": "poly", "closed": sh["closed"],
|
||
"pts": [(-x, y) for x, y in sh["pts"]]})
|
||
elif k == "circle":
|
||
out["shapes"].append({"k": "circle", "cx": -sh["cx"], "cy": sh["cy"], "r": sh["r"]})
|
||
elif k == "arc":
|
||
out["shapes"].append({"k": "arc", "sx": -sh["sx"], "sy": sh["sy"],
|
||
"mx": -sh["mx"], "my": sh["my"],
|
||
"ex": -sh["ex"], "ey": sh["ey"]})
|
||
out["texts"] = [dict(t, x=-t["x"]) for t in sym["texts"]]
|
||
for q in sym["pins"]:
|
||
out["pins"].append(dict(q, x=-q["x"], rot=(180 - int(q["rot"])) % 360))
|
||
return out
|
||
|
||
|
||
def mirrored_uuids():
|
||
"""哪些符号被 mirror=True 的实例用到 —— 只给这些生成镜像变体"""
|
||
tree = json.load(open(os.path.join(SRC, "_tree.json"), encoding="utf-8"))
|
||
out = set()
|
||
for b in tree["boards"]:
|
||
for p in b["pages"]:
|
||
g = json.load(open(os.path.join(SRC, b["board"], p["name"] + ".geom.json"), encoding="utf-8"))
|
||
for c in (g["prims"]["Component"] or []):
|
||
if c["ComponentType"] == "part" and c.get("Mirror"):
|
||
out.add((c.get("Symbol") or {}).get("uuid"))
|
||
return out
|
||
|
||
|
||
def main():
|
||
used = json.load(open(os.path.join(SRC, "_symbols_used.json"), encoding="utf-8"))
|
||
# netflag / netport 的符号 uuid + 网络,从几何数据里再扫一遍
|
||
tree = json.load(open(os.path.join(SRC, "_tree.json"), encoding="utf-8"))
|
||
flagsym = {} # net -> symbol uuid
|
||
for b in tree["boards"]:
|
||
for p in b["pages"]:
|
||
g = json.load(open(os.path.join(SRC, b["board"], p["name"] + ".geom.json"), encoding="utf-8"))
|
||
for c in (g["prims"]["Component"] or []):
|
||
if c["ComponentType"] == "netflag" and c.get("Net"):
|
||
flagsym.setdefault(c["Net"], (c.get("Component") or {}).get("uuid"))
|
||
|
||
os.makedirs(OUTDIR, exist_ok=True)
|
||
need_mirror = mirrored_uuids()
|
||
body, names, mirrors, missing = [], {}, {}, []
|
||
seen = collections.Counter()
|
||
|
||
for uuid, meta in sorted(used.items(), key=lambda kv: (kv[1]["name"] or "")):
|
||
path = os.path.join(SYMDIR, uuid + ".esym")
|
||
if not os.path.exists(path):
|
||
missing.append((uuid, meta["name"])); continue
|
||
sym = k_esym.parse(path)
|
||
base = safe((sym["name"] or meta["name"] or "").rsplit(".", 1)[0])
|
||
seen[base] += 1
|
||
kname = base if seen[base] == 1 else f"{base}_{seen[base]}"
|
||
names[uuid] = kname
|
||
body.append(symbol_sexp(kname, sym, meta["refs"][0], base))
|
||
if uuid in need_mirror:
|
||
mk = kname + "_MIRRORED"
|
||
mirrors[uuid] = mk
|
||
body.append(symbol_sexp(mk, mirror_sym(sym), meta["refs"][0], base))
|
||
|
||
# 电源符号:netflag 用的是 EasyEDA 系统符号,不在工程包里,所以按 KiCad 官方
|
||
# power 库的画法自建(引脚在原点、length 0、GND 朝下 270 / +V 朝上 90)。
|
||
# 旋转语义两边一致,实测见 easyeda-sch-geometry skill §2:
|
||
# Ground rot=0 -> 符号在下、引脚在上;KiCad GND 图形也在引脚下方,rot 0 对 rot 0。
|
||
# Power rot=0 -> 符号在上、引脚在下;KiCad +V 图形在引脚上方,同样 1:1。
|
||
powers = {}
|
||
for net in sorted(flagsym):
|
||
kname = safe(net)
|
||
powers[net] = kname
|
||
if net.upper().startswith("GND") or net.upper() in ("AGND", "PGND"):
|
||
gfx = [{"k": "poly", "closed": False,
|
||
"pts": [(0, 0), (0, -5), (5, -5), (0, -10), (-5, -5), (0, -5)]}]
|
||
ang, vy = 270, -15
|
||
else:
|
||
gfx = [{"k": "poly", "closed": False, "pts": [(-3, 5), (0, 10)]},
|
||
{"k": "poly", "closed": False, "pts": [(0, 10), (3, 5)]},
|
||
{"k": "poly", "closed": False, "pts": [(0, 0), (0, 10)]}]
|
||
ang, vy = 90, 14
|
||
sym = {"name": net, "bbox": [-5, -10, 5, 10], "shapes": gfx, "texts": [],
|
||
"pins": [{"x": 0, "y": 0, "len": 0, "rot": ang, "number": "1", "name": "",
|
||
"type": "POWER", "nameVisible": False, "numberVisible": False}]}
|
||
body.append(symbol_sexp(kname, sym, "#PWR", net, power=True,
|
||
descr=f"Power symbol creates a global label with name {net!r}"))
|
||
|
||
out = ["(kicad_symbol_lib", "\t(version 20251024)", f"\t(generator {q('phonerail-eda2kicad')})",
|
||
"\t(generator_version \"10.0\")"] + body + [")"]
|
||
p = os.path.join(OUTDIR, LIBNAME + ".kicad_sym")
|
||
open(p, "w", encoding="utf-8", newline="\n").write("\n".join(out) + "\n")
|
||
|
||
json.dump({"symbols": names, "powers": powers, "mirrors": mirrors},
|
||
open(os.path.join(SRC, "_kicad_symnames.json"), "w", encoding="utf-8"),
|
||
ensure_ascii=False, indent=1)
|
||
print(f"元件符号 {len(names)} 镜像变体 {len(mirrors)} 电源符号 {len(powers)} -> {p} ({os.path.getsize(p):,d} B)")
|
||
if missing:
|
||
print(f" ⚠ 找不到 .esym 的元件符号 {len(missing)}: {missing[:5]}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|