"""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