文件

201 行
8.3 KiB
Python

#!/usr/bin/env python3
"""生成 KiCad 封装库 kicad/lib/PhoneRail.pretty/*.kicad_mod。
来源:EasyEDA 工程包里的 .efoo。**必须**从工程包转,不能用 easyeda2kicad 按 LCSC 拉 ——
本工程有一批自制封装(触点盘 PAD-D1.5_MI8-KEY、BM24、BAT-FPC 等)根本没有 LCSC 料号。
单位:EasyEDA PCB 用 **mil**(实测 C0402 焊盘 19.685 mil = 0.500 mm、间距 0.84 mm)。
Y 轴:PCB 侧两边都是 Y 向下,**不翻**(只有原理图才需要翻,见 k_gen_sch.py)。
字段布局(实测):
["PAD", id, ?, net, layer, number, x, y, rot, 孔, 形状, ...]
孔 ["ROUND", d, d] / ["SLOT", w, h]
形状 ["RECT", w, h, r] / ["OVAL", w, h] / ["ELLIPSE", w, h] / ["POLY", 路径]
["POLY", id, ?, "", layer, width, 路径, ?]
["FILL", id, ?, "", layer, width, ?, [路径 或 ["CIRCLE",x,y,r]], ?]
路径: [x, y, "L", x2, y2, ..., "ARC", 扫掠角, x2, y2] 或 ["CIRCLE", x, y, r]
"""
import json, glob, math, os, re, sys
ROOT = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(ROOT, "eda_src", "epro", "FOOTPRINT")
OUTDIR = os.path.join(os.path.dirname(ROOT), "kicad", "lib", "PhoneRail.pretty")
MIL = 0.0254
# EasyEDA 层号 -> KiCad 层。49/50/51 是 EasyEDA 的装配层装饰(引脚焊接标记等),
# KiCad 里没有对应概念,带过去只会让封装变脏,故丢弃。
LAYER = {1: "F.Cu", 2: "B.Cu", 3: "F.SilkS", 4: "B.SilkS",
5: "F.Mask", 6: "B.Mask", 7: "F.Paste", 8: "B.Paste",
9: "F.Fab", 10: "B.Fab", 11: "Edge.Cuts", 13: "Dwgs.User",
14: "Cmts.User", 48: "F.CrtYd"}
DROP = {49, 50, 51, 12, 52}
def q(s):
return '"' + str(s if s is not None else "").replace("\\", "\\\\").replace('"', '\\"') + '"'
def mm(v):
x = round(float(v) * MIL, 5)
return (f"{x:.5f}".rstrip("0").rstrip(".")) or "0"
def arc_mid(ax, ay, bx, by, sweep):
"""EasyEDA 的「起点 + 扫掠角 + 终点」-> KiCad 的三点弧,返回弧中点(原始 mil 单位)。"""
th = math.radians(abs(sweep))
c = math.hypot(bx - ax, by - ay)
if c < 1e-9 or th < 1e-9:
return ((ax + bx) / 2, (ay + by) / 2)
R = c / (2 * math.sin(th / 2)) if math.sin(th / 2) > 1e-9 else c
h = math.sqrt(max(R * R - (c / 2) ** 2, 0.0))
mx, my = (ax + bx) / 2, (ay + by) / 2
nx, ny = -(by - ay) / c, (bx - ax) / c
best = None
for sign in (1, -1):
cx, cy = mx + sign * nx * h, my + sign * ny * h
a0 = math.atan2(ay - cy, ax - cx)
a1 = a0 + math.radians(sweep)
if abs((cx + R * math.cos(a1)) - bx) < 1e-3 and abs((cy + R * math.sin(a1)) - by) < 1e-3:
am = a0 + math.radians(sweep) / 2
best = (cx + R * math.cos(am), cy + R * math.sin(am))
break
return best or ((ax + bx) / 2, (ay + by) / 2)
def walk_path(path):
"""路径 -> [('line',x1,y1,x2,y2) | ('arc',x1,y1,mx,my,x2,y2) | ('circle',x,y,r)]"""
if path and path[0] == "CIRCLE":
return [("circle", path[1], path[2], path[3])]
out, i = [], 0
cx, cy = path[0], path[1]
i = 2
while i < len(path):
t = path[i]
if t == "L":
i += 1
while i + 1 < len(path) and not isinstance(path[i], str):
out.append(("line", cx, cy, path[i], path[i + 1]))
cx, cy = path[i], path[i + 1]; i += 2
elif t == "ARC":
sweep, nx, ny = path[i + 1], path[i + 2], path[i + 3]
mx, my = arc_mid(cx, cy, nx, ny, sweep)
out.append(("arc", cx, cy, mx, my, nx, ny))
cx, cy = nx, ny; i += 4
else:
i += 1
return out
def gr(kind, layer, width, seg):
w = mm(width) if width else "0.12"
if seg[0] == "line":
return (f'\t({kind}_line (start {mm(seg[1])} {mm(seg[2])}) (end {mm(seg[3])} {mm(seg[4])})\n'
f'\t\t(stroke (width {w}) (type solid)) (layer {q(layer)})\n\t)')
if seg[0] == "arc":
return (f'\t({kind}_arc (start {mm(seg[1])} {mm(seg[2])}) (mid {mm(seg[3])} {mm(seg[4])}) '
f'(end {mm(seg[5])} {mm(seg[6])})\n'
f'\t\t(stroke (width {w}) (type solid)) (layer {q(layer)})\n\t)')
if seg[0] == "circle":
return (f'\t({kind}_circle (center {mm(seg[1])} {mm(seg[2])}) '
f'(end {mm(seg[1] + seg[3])} {mm(seg[2])})\n'
f'\t\t(stroke (width {w}) (type solid)) (fill no) (layer {q(layer)})\n\t)')
return ""
def pad_sexp(r):
layer, num, x, y, rot = r[4], str(r[5]), r[6], r[7], r[8] or 0
hole, shape = r[9], r[10]
th = (layer == 12)
kind = "thru_hole" if th else "smd"
layers = '"*.Cu" "*.Mask"' if th else '"F.Cu" "F.Paste" "F.Mask"'
sh = shape[0] if isinstance(shape, list) else "RECT"
extra, prims = "", ""
if sh == "RECT":
w, h = shape[1], shape[2]
rad = shape[3] if len(shape) > 3 else 0
if rad:
form = "roundrect"
extra = f" (roundrect_rratio {round(min(rad / min(w, h), 0.5), 4)})"
else:
form = "rect"
size = f"(size {mm(w)} {mm(h)})"
elif sh in ("ELLIPSE", "OVAL"):
w, h = shape[1], shape[2]
form = "circle" if abs(w - h) < 1e-6 else "oval"
size = f"(size {mm(w)} {mm(h)})"
elif sh == "POLY":
form = "custom"
pts = shape[1]
seq = [v for v in pts if not isinstance(v, str)]
xs = seq[0::2]; ys = seq[1::2]
size = f"(size {mm(max(1.0, (max(xs)-min(xs))/8))} {mm(max(1.0, (max(ys)-min(ys))/8))})"
poly = " ".join(f"(xy {mm(px - x)} {mm(py - y)})" for px, py in zip(xs, ys))
extra = " (options (clearance outline) (anchor rect))"
prims = (f'\n\t\t(primitives\n\t\t\t(gr_poly (pts {poly})\n'
f'\t\t\t\t(width 0) (fill yes)\n\t\t\t)\n\t\t)')
else:
form = "rect"; size = "(size 0.5 0.5)"
drill = ""
if th and isinstance(hole, list):
if hole[0] == "SLOT":
drill = f" (drill oval {mm(hole[1])} {mm(hole[2])})"
elif hole[1]:
drill = f" (drill {mm(hole[1])})"
at = f"(at {mm(x)} {mm(y)}{'' if not rot else ' ' + str(int(rot))})"
return (f'\t(pad {q(num)} {kind} {form} {at} {size}{drill} (layers {layers}){extra}{prims}\n\t)')
def convert(path):
rows = [json.loads(l) for l in open(path, encoding="utf-8") if l.strip()]
head = next((r[1] for r in rows if r[0] == "HEAD"), {})
name = head.get("title") or os.path.basename(path)[:-5]
body, npad, warn = [], 0, []
has_th = False
for r in rows:
t = r[0]
if t == "PAD":
body.append(pad_sexp(r)); npad += 1
has_th |= (r[4] == 12)
elif t == "POLY":
lay = LAYER.get(r[4])
if r[4] in DROP or not lay:
continue
for seg in walk_path(r[6]):
body.append(gr("fp", lay, r[5], seg))
elif t == "FILL":
lay = LAYER.get(r[4])
if r[4] in DROP or not lay:
continue
for shp in (r[7] or []):
for seg in walk_path(shp):
body.append(gr("fp", lay, r[5], seg))
out = ["(footprint " + q(name), "\t(version 20260206)",
f"\t(generator {q('phonerail-eda2kicad')})", '\t(generator_version "10.0")',
'\t(layer "F.Cu")',
f'\t(descr {q("由 EasyEDA 工程「五路手机直供电源板」转换")})',
f'\t(attr {"through_hole" if has_th else "smd"})',
f'\t(property "Reference" "REF**"\n\t\t(at 0 -2 0)\n\t\t(layer "F.SilkS")\n'
f'\t\t(effects (font (size 1 1) (thickness 0.15)))\n\t)',
f'\t(property "Value" {q(name)}\n\t\t(at 0 2 0)\n\t\t(layer "F.Fab")\n'
f'\t\t(effects (font (size 1 1) (thickness 0.15)))\n\t)']
return name, "\n".join(out + body + [")"]) + "\n", npad
def main():
os.makedirs(OUTDIR, exist_ok=True)
seen, n = {}, 0
for f in sorted(glob.glob(os.path.join(SRC, "*.efoo"))):
name, txt, npad = convert(f)
safe = re.sub(r'[<>:"/\|?*\r\n]', "_", name)
if safe in seen:
continue # 同名封装在两块板里各存了一份,内容一致
seen[safe] = npad
open(os.path.join(OUTDIR, safe + ".kicad_mod"), "w", encoding="utf-8", newline="\n").write(txt)
n += 1
print(f"封装 {n} 个 -> {OUTDIR}")
return seen
if __name__ == "__main__":
main()