Add item hierarchy, BasicBoard, and DSN->board construction
Ports the data model of the board Item hierarchy (Item base, Pin, ObstacleArea/ConductionArea, Via, Trace) and BasicBoard (layers, nets, clearance, bounding box, items with query helpers), then build_board: the load-bearing integration that constructs a BasicBoard from a parsed DsnBoard. build_board maps layers -> LayerStructure, resolution -> transform, default clearance rule -> ClearanceMatrix, nets -> Nets plus a (component,pin)->net map, padstacks x placement -> Pin items, and keepouts -> ObstacleArea items. Rectangle pads become exact IntBoxes, convex polygon pads exact Simplexes, circle pads their bounding box (documented approximation); every pad is centred on its pin location so it contains that location by construction. Trace/Via are router-produced and lightweight here (an imported unrouted board has none). Validated on a real KiCad export (kicad_routable.dsn): layer/net/pin counts match the parsed DSN, every pin's pad shape contains its origin, every pin reports a valid net, and pin locations lie in the board bounding box. An oracle-gated test cross-checks that every net the reference FreeRouting JAR routes exists on the constructed board.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""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 = ""
|
||||
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)
|
||||
|
||||
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 power-plane conduction area (an obstacle carrying a net)."""
|
||||
|
||||
|
||||
@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))
|
||||
|
||||
|
||||
@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
|
||||
Reference in New Issue
Block a user