Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
670d2a9c4This file is pinned to the selected release. Download the original to open it in your design tool.
"""Read ``.kicad_pro`` net-class definitions and apply them to a SWIG board.
KiCad 7+ stores net-class definitions in the project file
(``net_settings.classes``), not the board. ``pcbnew.LoadBoard()`` reads only
the ``.kicad_pcb``, so a board loaded headless carries just the stock
Default class. ``pcbnew.ExportSpecctraDSN()`` then exports every net under a
single ``kicad_default`` DSN class at Default width/clearance, silently
dropping the user's class rules — a power net gets handed to Freerouting at
signal width with no warning (#302).
These helpers rebuild the board's ``NET_SETTINGS`` from the project file so
KiCad's own exporter sees the same classes the GUI would and natively emits
per-class ``(class ...)`` blocks, via padstacks, and rules. Verified against
real KiCad 10: applying a 2.0 mm "Power" class over two nets makes
``ExportSpecctraDSN`` write ``(class Power PWR PWR2 (circuit (use_via
"Via[0-1]_1200:600_um")) (rule (width 2000) (clearance 350)))``, register
the via padstack in the library, and drop the claimed nets from
``kicad_default`` — byte-identical to a project-loaded GUI export.
"""
import json
import logging
import os
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger("kicad_interface")
# net_settings.classes numeric fields (millimetres) -> NETCLASS setter names.
# Same field vocabulary as commands/routing.py's _NETCLASS_NUMERIC_FIELDS.
_NETCLASS_MM_SETTERS: Tuple[Tuple[str, str], ...] = (
("clearance", "SetClearance"),
("track_width", "SetTrackWidth"),
("via_diameter", "SetViaDiameter"),
("via_drill", "SetViaDrill"),
("microvia_diameter", "SetuViaDiameter"),
("microvia_drill", "SetuViaDrill"),
("diff_pair_width", "SetDiffPairWidth"),
("diff_pair_gap", "SetDiffPairGap"),
)
def load_project_net_classes(pro_path: Optional[str]) -> Optional[Dict[str, Any]]:
"""Parse ``net_settings`` from a ``.kicad_pro`` into a normalized dict.
Returns ``None`` when the file does not exist. Raises ``ValueError`` on
unreadable/unparseable JSON so the caller can surface the reason instead
of silently exporting without classes.
Result shape::
{
"classes": [ {name, track_width, clearance, ...}, ... ],
"patterns": [ (pattern, netclass), ... ],
"assignments": { net_name: [class_name, ...], ... },
}
"""
if not pro_path or not os.path.isfile(pro_path):
return None
try:
with open(pro_path, "r", encoding="utf-8") as handle:
data = json.load(handle)
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"cannot read project file {pro_path}: {exc}") from exc
net_settings = data.get("net_settings") or {}
classes: List[Dict[str, Any]] = [
cls
for cls in (net_settings.get("classes") or [])
if isinstance(cls, dict) and cls.get("name")
]
patterns: List[Tuple[str, str]] = []
for entry in net_settings.get("netclass_patterns") or []:
if isinstance(entry, dict) and entry.get("pattern") and entry.get("netclass"):
patterns.append((str(entry["pattern"]), str(entry["netclass"])))
# netclass_assignments maps net name -> list of class names (KiCad 9+
# serialization); tolerate a bare string value as well.
assignments: Dict[str, List[str]] = {}
raw_assignments = net_settings.get("netclass_assignments")
if isinstance(raw_assignments, dict):
for net, value in raw_assignments.items():
if isinstance(value, str):
names = [value]
elif isinstance(value, (list, tuple)):
names = [v for v in value if isinstance(v, str)]
else:
continue
if names:
assignments[str(net)] = names
return {"classes": classes, "patterns": patterns, "assignments": assignments}
def _apply_numeric_fields(pcbnew_mod: Any, netclass: Any, cls: Dict[str, Any]) -> bool:
"""Set the mm-valued fields present in ``cls`` on a NETCLASS. Returns
whether any field was applied."""
changed = False
for key, setter in _NETCLASS_MM_SETTERS:
value = cls.get(key)
if isinstance(value, (int, float)) and not isinstance(value, bool):
getattr(netclass, setter)(pcbnew_mod.FromMM(float(value)))
changed = True
return changed
def apply_net_classes_to_board(board: Any, settings: Dict[str, Any]) -> Dict[str, Any]:
"""Push parsed project net classes into ``board``'s ``NET_SETTINGS``.
May raise on a SWIG API mismatch (older KiCad builds); callers wrap this
and degrade to a warning rather than failing the export.
"""
import pcbnew
net_settings = board.GetDesignSettings().m_NetSettings
applied: List[str] = []
default_updated = False
for cls in settings.get("classes", []):
name = str(cls["name"])
if name == "Default":
# The Default class already exists; update it in place so its
# project-configured width/clearance reach the DSN's default rule.
default_updated = _apply_numeric_fields(pcbnew, net_settings.GetDefaultNetclass(), cls)
continue
netclass = pcbnew.NETCLASS(name)
_apply_numeric_fields(pcbnew, netclass, cls)
priority = cls.get("priority")
if isinstance(priority, int) and not isinstance(priority, bool):
netclass.SetPriority(priority)
net_settings.SetNetclass(name, netclass)
applied.append(name)
assignment_count = 0
for pattern, class_name in settings.get("patterns", []):
net_settings.SetNetclassPatternAssignment(pattern, class_name)
assignment_count += 1
# Explicit per-net assignments are expressed as exact-name patterns:
# SetNetclassLabelAssignment is not callable from Python (its argument is
# a C++ std::set<wxString>), and a literal net name is a wildcard pattern
# matching exactly itself. Nets assigned multiple classes keep only the
# last one (same-pattern assignments replace); composite-class merging is
# not modeled here.
for net_name, class_names in settings.get("assignments", {}).items():
for class_name in class_names:
net_settings.SetNetclassPatternAssignment(net_name, class_name)
assignment_count += 1
if hasattr(net_settings, "ClearAllCaches"):
net_settings.ClearAllCaches()
board.SynchronizeNetsAndNetClasses(False)
return {
"applied": applied,
"defaultUpdated": default_updated,
"assignments": assignment_count,
}