Files
rpmroute/src/freeroute/board/items.py
T
rsp2k 6e852a929b Hole spacing to a fixed via measured from where it is, with its own drill
A fixed via sits anywhere, not on a cell centre, and may have a bigger
drill than the router's via; checking hole spacing between cell centres
with one drill let the flight controller's routed 0.2 mm via come 0.19
mm from a hand-placed 0.25 mm one. The board now carries each fixed
via's drill (from KiCad's padstack name), and cells are checked against
fixed vias exactly.
2026-09-29 02:34:17 -06:00

138 lines
4.5 KiB
Python

"""Board items — ``Item`` base and the concrete item kinds.
Ports the *data model* of ``board/Item.java`` and its subclasses (``Pin``,
``ObstacleArea``/``ConductionArea``, ``Via``, ``PolylineTrace``). The routing
and search-tree machinery in the Java classes is out of scope here — these hold
the geometry and connectivity a :class:`~freeroute.board.board.BasicBoard`
needs and that the DSN-import invariants check.
Coordinates are board units (DSN units scaled by the resolution). Shapes are the
exact convex tiles / boxes from :mod:`freeroute.geometry`.
``Trace``/``Via`` are only produced by routing (an imported unrouted board has
none); they are lightweight here and the router phase fleshes out
``Trace`` with a proper :class:`~freeroute.geometry.Polyline` shape.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from freeroute.geometry import IntBox, IntPoint
__all__ = ["Item", "Pin", "ObstacleArea", "ConductionArea", "Via", "Trace"]
@dataclass
class Item:
"""Base class for everything placed on the board.
``net_nos`` are the board net numbers this item belongs to (empty for
unconnected obstacles); ``component_no`` is 0 for board-level items.
"""
net_nos: list[int] = field(default_factory=list)
clearance_class: int = 0
component_no: int = 0
fixed: bool = False
def contains_net(self, net_no: int) -> bool:
return net_no in self.net_nos
def is_obstacle(self) -> bool:
return False
@dataclass
class Pin(Item):
"""A component pin: a pad shape on one or more layers at a fixed location."""
name: str = ""
#: reference designator of the owning component, for diagnostics
component_ref: str = ""
padstack_name: str = ""
location: IntPoint = field(default_factory=lambda: IntPoint(0, 0))
#: layer indices the pad occupies (all signal layers when spanning)
layers: list[int] = field(default_factory=list)
#: the pad tile shape in absolute board coordinates
shape: object = None
def first_layer(self) -> int:
return self.layers[0] if self.layers else 0
def shape_contains_origin(self) -> bool:
"""Invariant helper: the pad shape contains the pin's own location."""
return self.shape is not None and self.shape.contains(self.location)
@dataclass
class ObstacleArea(Item):
"""A keepout / obstacle region on one layer, as a list of convex tiles."""
name: str | None = None
layer: int = 0
tiles: list = field(default_factory=list)
#: a DSN keepout blocks both; wire_keepout only traces (KiCad's "no
#: tracks" rule area, where vias are allowed), via_keepout only vias
blocks_wires: bool = True
blocks_vias: bool = True
#: False for a KiCad rule area: copper may not overlap it but may touch
#: it, since KiCad's DRC applies no clearance to a rule area. Every other
#: keepout may stand for copper (a net tie, a track kept as it is), so it
#: keeps the clearance unless named as a rule area.
clearance: bool = True
def is_obstacle(self) -> bool:
return True
def bounding_box(self) -> IntBox:
if not self.tiles:
return IntBox.empty()
box = self.tiles[0].bounding_box()
for tile in self.tiles[1:]:
box = box.union(tile.bounding_box())
return box
@dataclass
class ConductionArea(ObstacleArea):
"""A copper zone of one net (a DSN plane): not an obstacle to routing,
since KiCad refills it around other nets' copper, but copper of its net:
pads it touches are connected through it, and a route of its net ends on
it by placing a via inside it."""
#: the zone outline, for point containment
polygon: object = None
def is_obstacle(self) -> bool:
return False
@dataclass
class Via(Item):
"""A routed via at a location using a named padstack (router-produced)."""
padstack_name: str = ""
location: IntPoint = field(default_factory=lambda: IntPoint(0, 0))
#: layer indices the via's pads occupy, and the largest pad as a box
layers: list[int] = field(default_factory=list)
shape: object = None
#: drill diameter in board units, when the padstack name gives it
drill: int = 0
@dataclass
class Trace(Item):
"""A routed trace (router-produced).
Held as its corner points plus a half-width; the router phase attaches the
full :class:`~freeroute.geometry.Polyline` swept shape.
"""
layer: int = 0
half_width: int = 0
corners: list[IntPoint] = field(default_factory=list)
def is_empty(self) -> bool:
return len(self.corners) < 2