R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+200
@@ -0,0 +1,200 @@
|
||||
#!/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()
|
||||
在新工单中引用
屏蔽一个用户