Add KiCad cad-bridge JSON and an action plugin for finding focus.
Write pinscope-findings.json from the report, keep symbol uuid and child-sheet on the graph, and let the pcbnew plugin jump to a footprint or show the schematic uuid when IPC focus is unavailable. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""pinscope-cad-bridge JSON (E2) for the KiCad action plugin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from backend.pinscopex.models import CadIndexEntry, DesignGraph, Finding, ValidationReport
|
||||
|
||||
CAD_BRIDGE_VERSION = 1
|
||||
_PCB_RULE_PREFIXES = ("PS-PLC", "PS-SI", "PS-LAY", "PS-3W", "PS-CLR")
|
||||
|
||||
|
||||
def annotate_findings_cad(
|
||||
findings: list[Finding],
|
||||
cad_index: dict[str, CadIndexEntry] | None,
|
||||
) -> None:
|
||||
"""Fill cad_sheet/cad_uuid from the graph index when the finding omitted them."""
|
||||
if not cad_index:
|
||||
return
|
||||
for f in findings:
|
||||
entry = cad_index.get(f.designator)
|
||||
if not entry:
|
||||
continue
|
||||
if not f.cad_uuid and entry.uuid:
|
||||
f.cad_uuid = entry.uuid
|
||||
if not f.cad_sheet and entry.sheet:
|
||||
f.cad_sheet = entry.sheet
|
||||
|
||||
|
||||
def _pin_numbers(designator: str, pins: list[str]) -> list[str]:
|
||||
out: list[str] = []
|
||||
prefix = designator + "."
|
||||
for raw in pins:
|
||||
s = str(raw).strip()
|
||||
if not s:
|
||||
continue
|
||||
if s.upper().startswith(prefix.upper()):
|
||||
s = s[len(prefix):]
|
||||
out.append(s)
|
||||
return out
|
||||
|
||||
|
||||
def _target_kind(rule_id: str | None) -> str:
|
||||
rid = rule_id or ""
|
||||
if any(rid.startswith(p) for p in _PCB_RULE_PREFIXES):
|
||||
return "pcb"
|
||||
return "sch"
|
||||
|
||||
|
||||
def build_cad_bridge(
|
||||
report: ValidationReport,
|
||||
project_id: str,
|
||||
*,
|
||||
url_base: str = "",
|
||||
) -> dict:
|
||||
"""E2 `pinscope-cad-bridge` payload. Missing uuid/sheet stay empty strings."""
|
||||
findings: list[dict] = []
|
||||
for f in report.findings:
|
||||
fid = f.finding_id or ""
|
||||
url = ""
|
||||
if url_base and fid:
|
||||
sep = "&" if "?" in url_base else "?"
|
||||
url = f"{url_base}{sep}finding={fid}"
|
||||
findings.append({
|
||||
"rule_id": f.rule_id or f.source or "review",
|
||||
"ref": f.designator,
|
||||
"pins": _pin_numbers(f.designator, f.pins or []),
|
||||
"sheet": f.cad_sheet or "",
|
||||
"uuid": f.cad_uuid or "",
|
||||
"severity": (f.status or "WARNING").lower(),
|
||||
"message": f.finding,
|
||||
"url": url,
|
||||
"finding_id": fid,
|
||||
"net": f.net or "",
|
||||
"target": _target_kind(f.rule_id),
|
||||
})
|
||||
return {
|
||||
"version": CAD_BRIDGE_VERSION,
|
||||
"project_id": project_id,
|
||||
"findings": findings,
|
||||
}
|
||||
|
||||
|
||||
def write_cad_bridge(path: str | Path, payload: dict) -> None:
|
||||
Path(path).write_text(json.dumps(payload, indent=2) + "\n")
|
||||
|
||||
|
||||
def cad_index_from_graph(graph: DesignGraph) -> dict[str, CadIndexEntry]:
|
||||
return dict(graph.cad_index or {})
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
|
||||
from backend.pinscopex.utils import safe_mpn
|
||||
from backend.pinscopex.models import (
|
||||
CadIndexEntry,
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentModel,
|
||||
@@ -289,6 +290,8 @@ def build_graph(
|
||||
schematic_fields[ref] = {
|
||||
"mpn": extra.get("mpn"),
|
||||
"value": extra.get("value", ""),
|
||||
"cad_uuid": extra.get("cad_uuid") or "",
|
||||
"cad_sheet": extra.get("cad_sheet") or "",
|
||||
}
|
||||
entry = bom.setdefault(
|
||||
ref,
|
||||
@@ -410,9 +413,17 @@ def build_graph(
|
||||
pins=pin_connections,
|
||||
)
|
||||
|
||||
cad_index: dict[str, CadIndexEntry] = {}
|
||||
for ref, extra in schematic_fields.items():
|
||||
uuid = extra.get("cad_uuid") or ""
|
||||
sheet = extra.get("cad_sheet") or ""
|
||||
if uuid or sheet:
|
||||
cad_index[ref] = CadIndexEntry(uuid=uuid, sheet=sheet)
|
||||
|
||||
return DesignGraph(
|
||||
components=components,
|
||||
nets=nets,
|
||||
bom_fields=bom_fields,
|
||||
schematic_fields=schematic_fields,
|
||||
cad_index=cad_index,
|
||||
)
|
||||
|
||||
@@ -234,6 +234,12 @@ class Component(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class CadIndexEntry(BaseModel):
|
||||
"""KiCad symbol identity for plugin pan-and-zoom."""
|
||||
uuid: str = ""
|
||||
sheet: str = ""
|
||||
|
||||
|
||||
class DesignGraph(BaseModel):
|
||||
"""
|
||||
Bipartite design graph: Components <-> Nets.
|
||||
@@ -247,6 +253,7 @@ class DesignGraph(BaseModel):
|
||||
# KiCad property table vs uploaded BOM (empty on PADS/EDIF).
|
||||
bom_fields: dict[str, dict] = {}
|
||||
schematic_fields: dict[str, dict] = {}
|
||||
cad_index: dict[str, CadIndexEntry] = {}
|
||||
|
||||
# -- Traversal helpers --------------------------------------------------
|
||||
|
||||
|
||||
@@ -394,7 +394,10 @@ def _parse_kicad_sch_sheet(tree: Any) -> _SchSheet:
|
||||
elif n == "lcsc" and v:
|
||||
lcsc = v
|
||||
parts[ref] = footprint
|
||||
fields[ref] = {"value": value, "footprint": footprint, "mpn": mpn, "lcsc": lcsc}
|
||||
fields[ref] = {
|
||||
"value": value, "footprint": footprint, "mpn": mpn, "lcsc": lcsc,
|
||||
"cad_uuid": _val(sym, "uuid"),
|
||||
}
|
||||
lp = lib_pins.get(lib_id, {})
|
||||
for pin_el in _kids(sym, "pin"):
|
||||
num = str(pin_el[1]) if len(pin_el) > 1 else ""
|
||||
@@ -579,7 +582,10 @@ def parse_kicad_sch_project(
|
||||
if ref in parts:
|
||||
raise ValueError(f"Duplicate reference {ref} in {path.name}")
|
||||
parts[ref] = fp
|
||||
fields[ref] = sheet.fields.get(ref, {})
|
||||
fields[ref] = {
|
||||
**sheet.fields.get(ref, {}),
|
||||
"cad_sheet": path.name,
|
||||
}
|
||||
for name, pins in sheet.nets.items():
|
||||
scope = sheet.net_scope.get(name, "unnamed")
|
||||
if scope == "global":
|
||||
|
||||
Reference in New Issue
Block a user