Files
rpmroute/src/freeroute/dsn/model.py
T
rsp2k 354c89a8d5 Route with copper zones, and let vias into no-tracks areas
freeroute ignored the DSN's planes (KiCad's zones), so it never knew that
pads in a pour are joined by it. On the rc4 power stage those pads sit
inside "keep router off" areas as well, and every connection to them was
dropped without a word.

- Read (plane <net> <shape>) and put each zone on the board as a
  ConductionArea of its net. Zones are not obstacles: KiCad refills them
  around other nets' copper.
- The grid groups each net's pads and zone pieces that copper already
  joins (a pad in a zone, two zones sharing a pad) and routes only between
  groups. A connection may end on a zone by a via inside its copper, or by
  a track where tracks are allowed.
- wire_keepout now blocks traces only, as KiCad's "no tracks" rule area
  does; via_keepout only vias; keepout both, in the grid and in the exact
  verifier.

rc4 power stage, exact engine, resolution 2: 2 unconnected, down from 8
(FreeRouting JAR: 12), no DRC errors.
2026-09-27 11:54:04 -06:00

230 lines
5.9 KiB
Python

"""Typed board model produced by parsing a Specctra DSN file.
These dataclasses are the Python analogue of the data that FreeRouting's
``io/specctra/parser`` scope readers accumulate before constructing a
``RoutingBoard``. They stay at the DSN level (names, raw coordinates in DSN
units) — turning them into board geometry is a later port stage.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from .shapes import Area, Shape
__all__ = [
"ParserInfo",
"Resolution",
"Layer",
"WidthRule",
"ClearanceRule",
"StructureRules",
"Padstack",
"Pin",
"Image",
"ComponentPlace",
"ComponentPlacement",
"NetPin",
"Net",
"NetClass",
"Keepout",
"DsnBoard",
]
@dataclass
class ParserInfo:
"""The ``(parser ...)`` scope."""
string_quote: str = '"'
host_cad: str | None = None
host_version: str | None = None
constants: list[tuple[str, str]] = field(default_factory=list)
generated_by_freerouting: bool = False
@dataclass
class Resolution:
"""The ``(resolution <unit> <value>)`` scope. Defaults match FreeRouting."""
unit: str = "mil"
value: int = 100
@dataclass
class Layer:
"""A ``(layer ...)`` entry. ``index`` is the physical layer number, 0 = top."""
name: str
index: int
is_signal: bool = True
net_names: list[str] = field(default_factory=list)
@dataclass
class WidthRule:
value: float
@dataclass
class ClearanceRule:
value: float
#: class-pair names from ``(type a-b)`` / ``(type smd_smd)``; empty = default
class_pairs: list[str] = field(default_factory=list)
@dataclass
class StructureRules:
"""The default ``(rule ...)`` block plus control/snap settings."""
width_rules: list[WidthRule] = field(default_factory=list)
clearance_rules: list[ClearanceRule] = field(default_factory=list)
@dataclass
class Padstack:
"""A ``(padstack ...)`` — one or more per-layer pad shapes plus flags."""
name: str
shapes: list[Shape] = field(default_factory=list)
attach_allowed: bool = True
placed_absolute: bool = False
@dataclass
class Pin:
"""A pin within an image: ``(pin <padstack> [ (rotate d) ] <name> x y)``."""
padstack_name: str
name: str
x: float
y: float
rotation: float = 0.0
@dataclass
class Image:
"""A ``(image ...)`` — a component footprint definition."""
name: str
is_front: bool = True
pins: list[Pin] = field(default_factory=list)
outlines: list[Shape] = field(default_factory=list)
keepouts: list[Area] = field(default_factory=list)
via_keepouts: list[Area] = field(default_factory=list)
wire_keepouts: list[Area] = field(default_factory=list)
place_keepouts: list[Area] = field(default_factory=list)
@dataclass
class ComponentPlace:
"""One placed instance: ``(place <refdes> x y front|back rot ...)``.
``x`` / ``y`` are ``None`` when the component is declared but not yet placed.
"""
name: str
x: float | None = None
y: float | None = None
is_front: bool = True
rotation: float = 0.0
position_fixed: bool = False
part_number: str | None = None
@dataclass
class ComponentPlacement:
"""A ``(component <libname> (place ...) ...)`` group."""
lib_name: str
places: list[ComponentPlace] = field(default_factory=list)
@dataclass
class NetPin:
"""A ``Comp-Pin`` reference inside a net's ``(pins ...)`` list."""
component: str
pin: str
@dataclass
class Net:
"""A ``(net <name> [subnet] (pins ...))`` entry."""
name: str
subnet: int = 1
pins: list[NetPin] = field(default_factory=list)
@dataclass
class NetClass:
"""A ``(class <name> net... (circuit (use_via ...)) (rule ...))`` entry."""
name: str
net_names: list[str] = field(default_factory=list)
width_rules: list[WidthRule] = field(default_factory=list)
clearance_rules: list[ClearanceRule] = field(default_factory=list)
use_via: list[str] = field(default_factory=list)
use_layer: list[str] = field(default_factory=list)
via_rule: str | None = None
@dataclass
class Plane:
"""A ``(plane <net> <shape>)``: a copper zone of that net (KiCad's zones)."""
net: str
shape: Shape
@dataclass
class Keepout:
"""A structure-level keepout area with its kind."""
area: Area
kind: str = "keepout" # keepout | via_keepout | wire_keepout | place_keepout
@dataclass
class DsnBoard:
"""The fully parsed DSN file."""
name: str = ""
parser: ParserInfo = field(default_factory=ParserInfo)
resolution: Resolution = field(default_factory=Resolution)
unit: str = "mil"
layers: list[Layer] = field(default_factory=list)
#: copper zones, as planes of a net
planes: list[Plane] = field(default_factory=list)
#: bounding-box boundary shape (layer ``pcb``), if present
boundary: Shape | None = None
#: outline shapes (layer ``signal``)
outlines: list[Shape] = field(default_factory=list)
via_padstack_names: list[str] = field(default_factory=list)
structure_rules: StructureRules = field(default_factory=StructureRules)
snap_angle: str = "fortyfive_degree"
via_at_smd_allowed: bool = False
keepouts: list[Keepout] = field(default_factory=list)
padstacks: list[Padstack] = field(default_factory=list)
images: list[Image] = field(default_factory=list)
placements: list[ComponentPlacement] = field(default_factory=list)
nets: list[Net] = field(default_factory=list)
net_classes: list[NetClass] = field(default_factory=list)
# Convenience lookups -----------------------------------------------------
def layer_names(self) -> list[str]:
return [layer.name for layer in self.layers]
def net(self, name: str) -> Net | None:
for n in self.nets:
if n.name == name:
return n
return None
def padstack(self, name: str) -> Padstack | None:
for p in self.padstacks:
if p.name == name:
return p
return None