Extend Finding with rule_id/net/pins, flatten multi-sheet .kicad_sch, flag MPN mismatches, score simple_project, and parse .kicad_pcb without running layout DRC. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
"""KiCad `.kicad_pcb` ingest — footprints, pads, nets, segments, vias.
|
|
|
|
No SI/DRC. Schema validation stays complete without this file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from backend.pinscopex.models import (
|
|
LayoutFootprint,
|
|
LayoutGraph,
|
|
LayoutPad,
|
|
LayoutSegment,
|
|
LayoutVia,
|
|
)
|
|
from backend.pinscopex.parsers_kicad import (
|
|
_at,
|
|
_fnum,
|
|
_kid,
|
|
_kids,
|
|
_parse_sexp,
|
|
_rotate,
|
|
_tag,
|
|
_val,
|
|
)
|
|
|
|
|
|
def _xy(node: object, name: str) -> tuple[float, float]:
|
|
k = _kid(node, name)
|
|
if not k or len(k) < 3:
|
|
return 0.0, 0.0
|
|
return _fnum(k[1]), _fnum(k[2])
|
|
|
|
|
|
def _prop(node: object, key: str) -> str:
|
|
for p in _kids(node, "property"):
|
|
if len(p) >= 3 and str(p[1]) == key:
|
|
return str(p[2])
|
|
return ""
|
|
|
|
|
|
def _pad_net(pad: object) -> str:
|
|
n = _kid(pad, "net")
|
|
if n and len(n) >= 3:
|
|
return str(n[2])
|
|
return ""
|
|
|
|
|
|
def parse_kicad_pcb(path: str | Path) -> LayoutGraph:
|
|
p = Path(path)
|
|
tree = _parse_sexp(p.read_text(encoding="utf-8", errors="replace"))
|
|
if _tag(tree) != "kicad_pcb":
|
|
raise ValueError(f"Expected kicad_pcb, got {_tag(tree)!r}")
|
|
|
|
nets: dict[str, int] = {}
|
|
footprints: dict[str, LayoutFootprint] = {}
|
|
segments: list[LayoutSegment] = []
|
|
vias: list[LayoutVia] = []
|
|
|
|
for node in tree[1:]:
|
|
if not isinstance(node, list) or not node:
|
|
continue
|
|
tag = _tag(node)
|
|
if tag == "net" and len(node) >= 3 and not any(isinstance(x, list) and x and x[0] == "node" for x in node[1:]):
|
|
try:
|
|
code = int(_fnum(node[1]))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
name = str(node[2])
|
|
if name:
|
|
nets[name] = code
|
|
continue
|
|
if tag in {"footprint", "module"}:
|
|
fp_name = str(node[1]) if len(node) > 1 and not isinstance(node[1], list) else ""
|
|
fx, fy, frot = _at(node)
|
|
layer = _val(node, "layer")
|
|
ref = _prop(node, "Reference")
|
|
if not ref or ref.startswith("#"):
|
|
continue
|
|
pads: list[LayoutPad] = []
|
|
for pad in _kids(node, "pad"):
|
|
num = str(pad[1]) if len(pad) > 1 else ""
|
|
if not num:
|
|
continue
|
|
px, py, _ = _at(pad)
|
|
rx, ry = _rotate(px, py, frot)
|
|
pads.append(LayoutPad(
|
|
number=num,
|
|
x=fx + rx,
|
|
y=fy + ry,
|
|
net=_pad_net(pad),
|
|
))
|
|
footprints[ref] = LayoutFootprint(
|
|
reference=ref,
|
|
footprint=fp_name,
|
|
x=fx,
|
|
y=fy,
|
|
layer=layer,
|
|
pads=pads,
|
|
)
|
|
continue
|
|
if tag == "segment":
|
|
net_el = _kid(node, "net")
|
|
net_name = ""
|
|
if net_el and len(net_el) >= 2:
|
|
code = int(_fnum(net_el[1]))
|
|
net_name = next((n for n, c in nets.items() if c == code), str(code))
|
|
segments.append(LayoutSegment(
|
|
start=_xy(node, "start"),
|
|
end=_xy(node, "end"),
|
|
width=_fnum(_val(node, "width") or 0),
|
|
layer=_val(node, "layer"),
|
|
net=net_name,
|
|
))
|
|
continue
|
|
if tag == "via":
|
|
net_el = _kid(node, "net")
|
|
net_name = ""
|
|
if net_el and len(net_el) >= 2:
|
|
code = int(_fnum(net_el[1]))
|
|
net_name = next((n for n, c in nets.items() if c == code), str(code))
|
|
vx, vy, _ = _at(node)
|
|
vias.append(LayoutVia(x=vx, y=vy, net=net_name))
|
|
|
|
return LayoutGraph(
|
|
nets=nets,
|
|
footprints=footprints,
|
|
segments=segments,
|
|
vias=vias,
|
|
)
|