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.
540 lines
22 KiB
Python
540 lines
22 KiB
Python
"""Build a :class:`BasicBoard` from a parsed DSN :class:`DsnBoard`.
|
|
|
|
This is the load-bearing integration between :mod:`freeroute.dsn` and the board
|
|
model. It mirrors what FreeRouting's ``Structure.create_board`` / ``Library`` /
|
|
``Component`` / ``Network`` readers do collectively, but on the already-parsed
|
|
``DsnBoard``:
|
|
|
|
* layers -> :class:`LayerStructure`
|
|
* resolution -> :class:`CoordinateTransform` (``scale_factor = resolution``)
|
|
* default clearance rule -> :class:`ClearanceMatrix`
|
|
* nets -> :class:`Nets` (+ a ``(component, pin) -> net`` map)
|
|
* padstacks x placement -> :class:`Pin` items
|
|
* keepouts -> :class:`ObstacleArea` items
|
|
|
|
Pad geometry: a rectangle pad becomes an exact :class:`IntBox`; a circle pad its
|
|
bounding box (documented approximation pending an exact circle tile); a convex
|
|
polygon pad an exact :class:`Simplex`. Every pad is placed centred on its pin
|
|
location, so it contains that location by construction. Component rotation is
|
|
applied to the pin position; the pad tile itself is not rotated (irrelevant to
|
|
the containment invariant, and pads are centre-symmetric in practice).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from freeroute.dsn.model import DsnBoard
|
|
from freeroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape
|
|
from freeroute.geometry import FloatPoint, IntBox, IntPoint, IntVector, Simplex
|
|
from freeroute.geometry.polygon_shape import PolygonShape
|
|
|
|
from .board import BasicBoard
|
|
from .clearance import ClearanceMatrix
|
|
from .items import ConductionArea, ObstacleArea, Pin, Trace, Via
|
|
from .layer import Layer, LayerStructure
|
|
from .net import Nets
|
|
from .transform import CoordinateTransform
|
|
|
|
__all__ = ["build_board"]
|
|
|
|
|
|
def build_board(dsn: DsnBoard) -> BasicBoard:
|
|
"""Construct a :class:`BasicBoard` from a parsed :class:`DsnBoard`."""
|
|
layer_structure = LayerStructure([Layer(layer.name, layer.is_signal) for layer in dsn.layers])
|
|
scale = max(dsn.resolution.value, 1)
|
|
transform = CoordinateTransform(scale)
|
|
|
|
clearance = _build_clearance(dsn, transform)
|
|
nets, terminal_to_net = _build_nets(dsn)
|
|
bounding_box = _build_bounding_box(dsn, transform)
|
|
|
|
board = BasicBoard(layer_structure, nets, clearance, bounding_box)
|
|
board.outline = _build_outline(dsn, transform)
|
|
|
|
_insert_pins(dsn, board, transform, terminal_to_net)
|
|
_insert_keepouts(dsn, board, transform)
|
|
_insert_image_keepouts(dsn, board, transform)
|
|
_insert_zones(dsn, board, transform, nets)
|
|
_insert_wiring(dsn, board, transform, nets)
|
|
return board
|
|
|
|
|
|
# --- clearance / nets / bounds ----------------------------------------------
|
|
|
|
|
|
def _build_clearance(dsn: DsnBoard, transform: CoordinateTransform) -> ClearanceMatrix:
|
|
default_value = 0
|
|
for rule in dsn.structure_rules.clearance_rules:
|
|
if not rule.class_pairs: # the layer-wide default clearance
|
|
default_value = round(transform.dsn_to_board(rule.value))
|
|
break
|
|
return ClearanceMatrix.default_instance(default_value)
|
|
|
|
|
|
def _build_nets(dsn: DsnBoard) -> tuple[Nets, dict[tuple[str, str], int]]:
|
|
nets = Nets()
|
|
terminal_to_net: dict[tuple[str, str], int] = {}
|
|
for dsn_net in dsn.nets:
|
|
net = nets.add(dsn_net.name, dsn_net.subnet, contains_plane=False)
|
|
net.pins = [(p.component, p.pin) for p in dsn_net.pins]
|
|
for pin in dsn_net.pins:
|
|
terminal_to_net[(pin.component, pin.pin)] = net.net_number
|
|
return nets, terminal_to_net
|
|
|
|
|
|
def _build_bounding_box(dsn: DsnBoard, transform: CoordinateTransform) -> IntBox:
|
|
coords: list[float] = []
|
|
shapes: list[Shape] = []
|
|
if dsn.boundary is not None:
|
|
shapes.append(dsn.boundary)
|
|
shapes.extend(dsn.outlines)
|
|
for shape in shapes:
|
|
coords.extend(_shape_coords(shape))
|
|
if not coords:
|
|
return IntBox.empty()
|
|
xs = coords[0::2]
|
|
ys = coords[1::2]
|
|
llx = round(transform.dsn_to_board(min(xs)))
|
|
lly = round(transform.dsn_to_board(min(ys)))
|
|
urx = round(transform.dsn_to_board(max(xs)))
|
|
ury = round(transform.dsn_to_board(max(ys)))
|
|
return IntBox(llx, lly, urx, ury)
|
|
|
|
|
|
def _build_outline(dsn: DsnBoard, transform: CoordinateTransform):
|
|
"""The DSN boundary as a polygon, or None when it is not a polygon."""
|
|
from .outline import BoardOutline
|
|
|
|
shape = dsn.boundary
|
|
if not isinstance(shape, (Polygon, Path)):
|
|
return None
|
|
c = shape.coords
|
|
pts = [transform.dsn_to_board_point(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
|
return BoardOutline(pts) if len(pts) >= 3 else None
|
|
|
|
|
|
def _shape_coords(shape: Shape) -> list[float]:
|
|
if isinstance(shape, Rectangle):
|
|
return list(shape.coords)
|
|
if isinstance(shape, Circle):
|
|
r = shape.diameter / 2
|
|
return [shape.center_x - r, shape.center_y - r, shape.center_x + r, shape.center_y + r]
|
|
if isinstance(shape, (Polygon, Path)):
|
|
return list(shape.coords)
|
|
return []
|
|
|
|
|
|
# --- pins --------------------------------------------------------------------
|
|
|
|
|
|
def _insert_pins(
|
|
dsn: DsnBoard,
|
|
board: BasicBoard,
|
|
transform: CoordinateTransform,
|
|
terminal_to_net: dict[tuple[str, str], int],
|
|
) -> None:
|
|
images = {img.name: img for img in dsn.images}
|
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
|
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
|
|
|
|
for placement in dsn.placements:
|
|
image = images.get(placement.lib_name)
|
|
if image is None:
|
|
continue
|
|
for place in placement.places:
|
|
if place.x is None or place.y is None:
|
|
continue # component declared but not placed
|
|
for dsn_pin in image.pins:
|
|
location = _pin_location(place, dsn_pin, transform)
|
|
padstack = dsn.padstack(dsn_pin.padstack_name)
|
|
shape, layers = _pad_shape_and_layers(
|
|
padstack, place, dsn_pin, location, transform, layer_no, signal_layers
|
|
)
|
|
net_no = terminal_to_net.get((place.name, dsn_pin.name))
|
|
pin = Pin(
|
|
net_nos=[net_no] if net_no is not None else [],
|
|
component_no=id(place) & 0xFFFFFF,
|
|
fixed=True,
|
|
name=dsn_pin.name,
|
|
component_ref=place.name,
|
|
padstack_name=dsn_pin.padstack_name,
|
|
location=location,
|
|
layers=layers,
|
|
shape=shape,
|
|
)
|
|
board.insert(pin)
|
|
|
|
|
|
def _to_board_offset(place, x: float, y: float, pin_rotation: float = 0.0) -> FloatPoint:
|
|
"""Image-relative DSN offset -> board-oriented DSN offset for a placed part.
|
|
|
|
The chain is: the pin's own rotation (pad shapes only), then the mirror of
|
|
a back-side part (x -> -x), then the part's rotation. Pin positions use the
|
|
same chain without the pin rotation, so pads and positions always agree.
|
|
"""
|
|
p = FloatPoint(x, y)
|
|
if pin_rotation:
|
|
p = p.rotate(math.radians(pin_rotation), FloatPoint(0.0, 0.0))
|
|
if not place.is_front:
|
|
p = FloatPoint(-p.x, p.y)
|
|
if place.rotation:
|
|
p = p.rotate(math.radians(place.rotation), FloatPoint(0.0, 0.0))
|
|
return p
|
|
|
|
|
|
def _pin_location(place, dsn_pin, transform: CoordinateTransform) -> IntPoint:
|
|
"""Absolute board location of a pin: component origin + placed pin offset."""
|
|
rel = _to_board_offset(place, dsn_pin.x, dsn_pin.y)
|
|
return transform.dsn_to_board_point(place.x + rel.x, place.y + rel.y)
|
|
|
|
|
|
def _pad_shape_and_layers(
|
|
padstack,
|
|
place,
|
|
dsn_pin,
|
|
location: IntPoint,
|
|
transform: CoordinateTransform,
|
|
layer_no: dict[str, int],
|
|
signal_layers: list[int],
|
|
) -> tuple[object, list[int]]:
|
|
"""A pad tile around ``location`` plus the layer indices it occupies.
|
|
|
|
A back-side part's pads move to the mirrored copper layer (F.Cu -> B.Cu on
|
|
a two-layer board), and the pad outline is placed through the same
|
|
rotation and mirror as the pin itself.
|
|
"""
|
|
if padstack is None or not padstack.shapes:
|
|
# degenerate: a zero-size box at the location (still contains it)
|
|
return IntBox(location.x, location.y, location.x, location.y), list(signal_layers)
|
|
|
|
layers: set[int] = set()
|
|
for shape in padstack.shapes:
|
|
if shape.layer in ("signal", "pcb"):
|
|
layers.update(signal_layers)
|
|
elif shape.layer in layer_no:
|
|
layers.add(layer_no[shape.layer])
|
|
if not layers:
|
|
layers = set(signal_layers)
|
|
if not place.is_front and signal_layers:
|
|
top, bottom = min(signal_layers), max(signal_layers)
|
|
layers = {top + bottom - layer if layer in signal_layers else layer for layer in layers}
|
|
|
|
# The largest of the padstack's shapes: per-layer shapes of a through-hole
|
|
# pad usually match, and when they do not the bigger one is the safe side.
|
|
shape = max(padstack.shapes, key=_shape_extent)
|
|
tile = _placed_pad_tile(shape, place, dsn_pin, transform)
|
|
tile = tile.translate_by(IntVector(location.x, location.y))
|
|
return tile, sorted(layers)
|
|
|
|
|
|
def _shape_extent(shape: Shape) -> float:
|
|
coords = _shape_coords(shape)
|
|
if isinstance(shape, Circle):
|
|
return shape.diameter ** 2
|
|
if len(coords) < 4:
|
|
return 0.0
|
|
xs, ys = coords[0::2], coords[1::2]
|
|
return (max(xs) - min(xs)) * (max(ys) - min(ys))
|
|
|
|
|
|
def _placed_pad_tile(shape: Shape, place, dsn_pin, transform: CoordinateTransform):
|
|
"""The pad outline, rotated and mirrored with its part, relative to the pin."""
|
|
if isinstance(shape, Circle):
|
|
centre = _to_board_offset(place, shape.center_x, shape.center_y, dsn_pin.rotation)
|
|
r = round(transform.dsn_to_board(shape.diameter / 2))
|
|
cx = round(transform.dsn_to_board(centre.x))
|
|
cy = round(transform.dsn_to_board(centre.y))
|
|
return IntBox(cx - r, cy - r, cx + r, cy + r)
|
|
if isinstance(shape, Rectangle):
|
|
x0, y0, x1, y1 = shape.coords[:4]
|
|
corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
|
elif isinstance(shape, (Polygon, Path)):
|
|
c = shape.coords
|
|
corners = [(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
|
else:
|
|
return _pad_tile(shape, transform)
|
|
# An oval pad is a path with a width (the aperture). Pad the centreline
|
|
# out by half the width in every direction; the square ends cover more
|
|
# than the round ones do, which is the safe side.
|
|
half = getattr(shape, "width", 0.0) / 2 if isinstance(shape, Path) else 0.0
|
|
if half:
|
|
corners = [(x + dx, y + dy) for x, y in corners for dx in (-half, half) for dy in (-half, half)]
|
|
pts = []
|
|
for x, y in corners:
|
|
p = _to_board_offset(place, x, y, dsn_pin.rotation)
|
|
pts.append(IntPoint(round(transform.dsn_to_board(p.x)), round(transform.dsn_to_board(p.y))))
|
|
if len(pts) >= 3:
|
|
simplex = Simplex.from_corners(pts)
|
|
if not simplex.is_empty():
|
|
return simplex
|
|
xs = [p.x for p in pts] or [0]
|
|
ys = [p.y for p in pts] or [0]
|
|
return IntBox(min(xs), min(ys), max(xs), max(ys))
|
|
|
|
|
|
def _pad_tile(shape: Shape, transform: CoordinateTransform):
|
|
"""A board-relative pad tile from a DSN padstack shape (centred near origin)."""
|
|
if isinstance(shape, Rectangle):
|
|
c = [round(transform.dsn_to_board(v)) for v in shape.coords[:4]]
|
|
return IntBox(min(c[0], c[2]), min(c[1], c[3]), max(c[0], c[2]), max(c[1], c[3]))
|
|
if isinstance(shape, Circle):
|
|
r = round(transform.dsn_to_board(shape.diameter / 2))
|
|
cx = round(transform.dsn_to_board(shape.center_x))
|
|
cy = round(transform.dsn_to_board(shape.center_y))
|
|
return IntBox(cx - r, cy - r, cx + r, cy + r) # bounding-box approximation
|
|
if isinstance(shape, (Polygon, Path)):
|
|
coords = shape.coords
|
|
pts = [
|
|
IntPoint(
|
|
round(transform.dsn_to_board(coords[i])),
|
|
round(transform.dsn_to_board(coords[i + 1])),
|
|
)
|
|
for i in range(0, len(coords) - 1, 2)
|
|
]
|
|
if len(pts) >= 3:
|
|
simplex = Simplex.from_corners(pts)
|
|
if not simplex.is_empty():
|
|
return simplex
|
|
xs = [p.x for p in pts] or [0]
|
|
ys = [p.y for p in pts] or [0]
|
|
return IntBox(min(xs), min(ys), max(xs), max(ys))
|
|
return IntBox(0, 0, 0, 0)
|
|
|
|
|
|
# --- keepouts ----------------------------------------------------------------
|
|
|
|
|
|
def _insert_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform) -> None:
|
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
|
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
|
|
|
|
for keepout in dsn.keepouts:
|
|
# A place_keepout only restricts where parts go, not copper.
|
|
if keepout.kind == "place_keepout":
|
|
continue
|
|
area = keepout.area
|
|
if area is None or area.border is None:
|
|
continue
|
|
tiles = _area_border_tiles(area.border, transform)
|
|
if not tiles:
|
|
continue
|
|
border_layer = area.border.layer
|
|
if border_layer in ("signal", "pcb"):
|
|
layers = signal_layers
|
|
elif border_layer in layer_no:
|
|
layers = [layer_no[border_layer]]
|
|
else:
|
|
layers = signal_layers
|
|
for layer_index in layers:
|
|
board.insert(
|
|
ObstacleArea(
|
|
name=keepout.area.name,
|
|
layer=layer_index,
|
|
tiles=tiles,
|
|
blocks_wires=keepout.kind != "via_keepout",
|
|
blocks_vias=keepout.kind != "wire_keepout",
|
|
clearance=not is_rule_area(keepout.area.name),
|
|
)
|
|
)
|
|
|
|
|
|
#: a structure keepout named with this prefix is a KiCad rule area (mckicad
|
|
#: names them on export); it is checked for overlap only
|
|
RULE_AREA_PREFIX = "rule_area"
|
|
|
|
|
|
def is_rule_area(name: str | None) -> bool:
|
|
return bool(name) and name.startswith(RULE_AREA_PREFIX)
|
|
|
|
|
|
def _area_border_tiles(shape: Shape, transform: CoordinateTransform) -> list:
|
|
"""Convex board tiles covering a keepout border shape.
|
|
|
|
Polygons are split into convex pieces rather than wrapped in one convex
|
|
tile: KiCad exports a rule area with a hole (a no-track ring along the
|
|
board edge, say) as a single keyhole polygon, and its convex hull is the
|
|
whole board.
|
|
"""
|
|
if isinstance(shape, (Polygon, Path)):
|
|
coords = shape.coords
|
|
pts = [
|
|
IntPoint(
|
|
round(transform.dsn_to_board(coords[i])),
|
|
round(transform.dsn_to_board(coords[i + 1])),
|
|
)
|
|
for i in range(0, len(coords) - 1, 2)
|
|
]
|
|
if len(pts) >= 3:
|
|
pieces = [t for t in PolygonShape(pts).split_to_convex() if not t.is_empty()]
|
|
if pieces:
|
|
return pieces
|
|
tile = _area_border_tile(shape, transform)
|
|
return [tile] if tile is not None else []
|
|
|
|
|
|
def _area_border_tile(shape: Shape, transform: CoordinateTransform):
|
|
"""Convert an absolute-coordinate keepout border shape to one board tile."""
|
|
if isinstance(shape, Rectangle):
|
|
c = [round(transform.dsn_to_board(v)) for v in shape.coords[:4]]
|
|
return IntBox(min(c[0], c[2]), min(c[1], c[3]), max(c[0], c[2]), max(c[1], c[3]))
|
|
if isinstance(shape, Circle):
|
|
r = round(transform.dsn_to_board(shape.diameter / 2))
|
|
cx = round(transform.dsn_to_board(shape.center_x))
|
|
cy = round(transform.dsn_to_board(shape.center_y))
|
|
return IntBox(cx - r, cy - r, cx + r, cy + r)
|
|
if isinstance(shape, (Polygon, Path)):
|
|
coords = shape.coords
|
|
pts = [
|
|
IntPoint(
|
|
round(transform.dsn_to_board(coords[i])),
|
|
round(transform.dsn_to_board(coords[i + 1])),
|
|
)
|
|
for i in range(0, len(coords) - 1, 2)
|
|
]
|
|
if len(pts) >= 3:
|
|
simplex = Simplex.from_corners(pts)
|
|
if not simplex.is_empty():
|
|
return simplex
|
|
xs = [p.x for p in pts] or [0]
|
|
ys = [p.y for p in pts] or [0]
|
|
return IntBox(min(xs), min(ys), max(xs), max(ys))
|
|
return None
|
|
|
|
|
|
def _insert_image_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform) -> None:
|
|
"""Keepouts declared inside a footprint, placed with each instance.
|
|
|
|
KiCad exports copper that has no pin number this way, for instance the
|
|
plated ring of a mounting hole, so skipping them leaves copper the router
|
|
cannot see.
|
|
"""
|
|
images = {img.name: img for img in dsn.images}
|
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
|
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
|
|
for placement in dsn.placements:
|
|
image = images.get(placement.lib_name)
|
|
if image is None:
|
|
continue
|
|
areas = [
|
|
*((a, True, True) for a in image.keepouts),
|
|
*((a, False, True) for a in image.via_keepouts),
|
|
*((a, True, False) for a in image.wire_keepouts),
|
|
]
|
|
if not areas:
|
|
continue
|
|
for place in placement.places:
|
|
if place.x is None or place.y is None:
|
|
continue
|
|
for area, blocks_wires, blocks_vias in areas:
|
|
border = area.border
|
|
if border is None:
|
|
continue
|
|
tiles = _placed_area_tiles(border, place, transform)
|
|
if not tiles:
|
|
continue
|
|
if border.layer in ("signal", "pcb") or border.layer not in layer_no:
|
|
layers = list(signal_layers)
|
|
else:
|
|
layers = [layer_no[border.layer]]
|
|
if not place.is_front and signal_layers:
|
|
top, bottom = min(signal_layers), max(signal_layers)
|
|
layers = [top + bottom - layer for layer in layers]
|
|
for layer_index in layers:
|
|
board.insert(
|
|
ObstacleArea(
|
|
name=area.name, layer=layer_index, tiles=tiles,
|
|
blocks_wires=blocks_wires, blocks_vias=blocks_vias,
|
|
)
|
|
)
|
|
|
|
|
|
def _placed_area_tiles(shape: Shape, place, transform: CoordinateTransform) -> list:
|
|
"""An image-relative area border placed with its part, as convex board tiles."""
|
|
def to_board(x: float, y: float) -> IntPoint:
|
|
p = _to_board_offset(place, x, y)
|
|
return transform.dsn_to_board_point(place.x + p.x, place.y + p.y)
|
|
|
|
if isinstance(shape, Circle):
|
|
c = to_board(shape.center_x, shape.center_y)
|
|
r = round(transform.dsn_to_board(shape.diameter / 2))
|
|
return [IntBox(c.x - r, c.y - r, c.x + r, c.y + r)]
|
|
if isinstance(shape, Rectangle):
|
|
x0, y0, x1, y1 = shape.coords[:4]
|
|
pts = [to_board(x, y) for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1))]
|
|
elif isinstance(shape, (Polygon, Path)):
|
|
c = shape.coords
|
|
pts = [to_board(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
|
else:
|
|
return []
|
|
if len(pts) < 3:
|
|
return []
|
|
return [tile for tile in PolygonShape(pts).split_to_convex() if not tile.is_empty()]
|
|
|
|
|
|
def _insert_wiring(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
|
"""Tracks and vias already on the board, as fixed copper of their nets."""
|
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
|
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
|
|
numbers = {net.name: net.net_number for net in nets}
|
|
for wire in dsn.wires:
|
|
if wire.net not in numbers or wire.layer not in layer_no:
|
|
continue
|
|
c = wire.coords
|
|
corners = [transform.dsn_to_board_point(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
|
if len(corners) < 2:
|
|
continue
|
|
board.insert(
|
|
Trace(
|
|
net_nos=[numbers[wire.net]], fixed=True, layer=layer_no[wire.layer],
|
|
half_width=max(round(transform.dsn_to_board(wire.width / 2)), 1), corners=corners,
|
|
)
|
|
)
|
|
for via in dsn.vias:
|
|
padstack = dsn.padstack(via.padstack)
|
|
if via.net not in numbers or padstack is None or not padstack.shapes:
|
|
continue
|
|
layers: set[int] = set()
|
|
for shape in padstack.shapes:
|
|
if shape.layer in ("signal", "pcb"):
|
|
layers.update(signal_layers)
|
|
elif shape.layer in layer_no:
|
|
layers.add(layer_no[shape.layer])
|
|
location = transform.dsn_to_board_point(via.x, via.y)
|
|
pad = _pad_tile(max(padstack.shapes, key=_shape_extent), transform)
|
|
box = pad.bounding_box()
|
|
board.insert(
|
|
Via(
|
|
net_nos=[numbers[via.net]], fixed=True, padstack_name=via.padstack,
|
|
location=location, layers=sorted(layers or signal_layers),
|
|
shape=IntBox(box.ll.x + location.x, box.ll.y + location.y,
|
|
box.ur.x + location.x, box.ur.y + location.y),
|
|
)
|
|
)
|
|
|
|
|
|
def _insert_zones(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
|
"""Each DSN plane as a ConductionArea of its net on its layer."""
|
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
|
numbers = {net.name: net.net_number for net in nets}
|
|
for plane in dsn.planes:
|
|
shape = plane.shape
|
|
if plane.net not in numbers or shape.layer not in layer_no:
|
|
continue
|
|
if not isinstance(shape, (Polygon, Path)):
|
|
continue
|
|
c = shape.coords
|
|
pts = [transform.dsn_to_board_point(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
|
if len(pts) < 3:
|
|
continue
|
|
polygon = PolygonShape(pts)
|
|
tiles = [t for t in polygon.split_to_convex() if not t.is_empty()]
|
|
if not tiles:
|
|
continue
|
|
board.insert(
|
|
ConductionArea(
|
|
net_nos=[numbers[plane.net]], name=plane.net, layer=layer_no[shape.layer],
|
|
tiles=tiles, polygon=polygon, blocks_wires=False, blocks_vias=False,
|
|
)
|
|
)
|