Files
rpmroute/src/freeroute/dsn/model.py
T
rsp2k 8a50bde531 Read the DSN's wiring as fixed copper of its nets
Tracks and vias already on the board were ignored, so other nets could
be routed straight through them (the flight controller's 82 GND
stitching vias and its guard tracks). They are now copper of their net:
other nets keep a pad's distance from them in the grid, the exact check
and the room engine, and they add no connections to route.
2026-09-28 21:06:35 -06:00

255 lines
6.5 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",
"WiringVia",
"WiringWire",
"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 WiringWire:
"""A track already on the board: ``(wire (path LAYER WIDTH x y ...) (net N))``."""
net: str
layer: str
width: float
coords: list[float] = field(default_factory=list)
@dataclass
class WiringVia:
"""A via already on the board: ``(via PADSTACK x y (net N))``."""
net: str
padstack: str
x: float
y: float
@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)
#: copper already routed (the ``wiring`` scope), kept fixed
wires: list[WiringWire] = field(default_factory=list)
vias: list[WiringVia] = 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