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.
138 lines
4.5 KiB
Python
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
|