from __future__ import annotations
from typing import override
from jitx.feature import Soldermask, KeepOut
from jitx.layerindex import LayerSet
from jitx.shapes.composites import (
Bounds,
bounds_area,
buffer_bounds,
rectangle_from_bounds,
)
from . import ApplyToMixin, LandpatternGenerator, LandpatternProvider
DEF_KEEPOUT_LAYERS = LayerSet(0)
INFINITY = float("inf")
[docs]
class KeepoutGenerator:
[docs]
def make_keepout(self, target: KeepoutGeneratorMixin) -> KeepOut:
raise NotImplementedError(f"{self.__class__.__name__}.make_keepout")
[docs]
class KeepoutGeneratorMixin(
ApplyToMixin,
LandpatternProvider,
):
__generator: KeepoutGenerator | None = None
_keepout: KeepOut | None = None
[docs]
def keepout(self, generator: KeepoutGenerator):
"""Add a keepout generator to the landpattern"""
self.__generator = generator
return self
@override
def _build(self):
# wipe any existing courtyard to prevent re-discovery on subsequent runs
self._keepout = None
super()._build()
@override
def _build_decorate(self):
super()._build_decorate()
if self.__generator is not None:
self._keepout = self.__generator.make_keepout(self)
[docs]
class IntraKeepoutGenerator(KeepoutGenerator):
"""Intra-Package Keepout Generator
This class generates a rectangular keepout which fills the interstitial
region between the pads of a landpattern.
"""
def __init__(
self,
*,
vertical: bool = False,
horizontal: bool = False,
layers: LayerSet | None = None,
keepout_adj: float | tuple[float, float] | None = None,
):
"""Intra-package Keepout Generator
Initializes a keepout feature in the interstitial region between the
interior bounds of the pads of a landpattern. This is defined as the
region between the lowest upper bound and the highest lower bound of any
pad along each dimension. Optionally, the keepout can be extended to the
full bounds in either dimension (i.e. lowest lower bound to highest
upper bound of any pad) using the ``vertical`` and ``horizontal`` flags.
This is useful for packages such as SOICs, where the interstitial region
would otherwise stop short of the edge at the inside of the end pads.
Args:
vertical: If True, the keepout will be extended to the vertical
bounds.
horizontal: If True, the keepout will be extended to the horizontal
bounds.
layers: optional, layers to use for the keepout. If unspecified,
the default is to only generate on the top layer.
keepout_adj: optional, amount to buffer the interstital region by
to generate the keepout. If this is a tuple, the first element
is the horizontal buffer and the second element is the vertical
buffer. This is usually a negative value. If unspecified, the
default value is 0.0. This does not affect dimensions which are
extended to the full bounds (i.e. the height if ``vertical`` is
True, or the width if ``horizontal`` is True).
"""
self.__vertical = vertical
self.__horizontal = horizontal
self.__layers = layers
self.__keepout_adj = keepout_adj
[docs]
def make_keepout(self, target: KeepoutGeneratorMixin) -> KeepOut:
match self.__keepout_adj:
case float():
adj_x = self.__keepout_adj
adj_y = self.__keepout_adj
case tuple():
adj_x, adj_y = self.__keepout_adj
case _:
adj_x = 0.0
adj_y = 0.0
# ``rearrange_fn`` swaps, for each axis, the upper and lower extents of
# the pad shape along that axis. Unioning these rearranged extents
# gives the bounds of the interstitial region (minimum upper extent to
# maximum lower extent).
# However, if the keepout is extended to the full extent in one or more
# axes by using the ``vertical`` or ``horizontal`` flags, the bounds for
# those axes should be left unswapped instead. This match statement sets
# the correct ``rearrange_fn`` for each case.
# Also, the adjustment is set to 0.0 for axes which are fully-extended
match self.__vertical, self.__horizontal:
case True, True:
rearrange_fn = no_transpose
adj_x = 0.0
adj_y = 0.0
case True, False:
rearrange_fn = transpose_x
adj_y = 0.0
case False, True:
rearrange_fn = transpose_y
adj_x = 0.0
case _:
rearrange_fn = transpose_bounds
bounds = (INFINITY, INFINITY, -INFINITY, -INFINITY)
for shape in target._applies_to_shapes(Soldermask):
shape_bounds = shape.to_shapely().bounds
re_bounds = rearrange_fn(shape_bounds)
bounds = bounds_union(bounds, re_bounds)
if bounds_area(bounds) <= 0.0:
raise ValueError("Interstitial keepout bounds are empty")
bounds = buffer_bounds(bounds, (adj_x, adj_y))
if bounds_area(bounds) <= 0.0:
raise ValueError("Not enough space to construct interstitial keepout")
keepout_shape = rectangle_from_bounds(bounds)
if self.__layers is None:
layers = DEF_KEEPOUT_LAYERS
else:
layers = self.__layers
return KeepOut(keepout_shape, layers=layers, pour=True)
[docs]
class IntraKeepout(KeepoutGeneratorMixin, LandpatternGenerator):
"""A keepout generator mixin with default IntraKeepout generator."""
@override
def __base_init__(self):
super().__base_init__()
self.keepout(IntraKeepoutGenerator())
[docs]
def bounds_union(b1: Bounds, b2: Bounds) -> Bounds:
return (
min(b1[0], b2[0]),
min(b1[1], b2[1]),
max(b1[2], b2[2]),
max(b1[3], b2[3]),
)
[docs]
def transpose_bounds(b: Bounds) -> Bounds:
"""Flips the min/max sides of a bounds"""
return (b[2], b[3], b[0], b[1])
[docs]
def transpose_x(b: Bounds) -> Bounds:
"""Flips the min/max x values of a bounds"""
return (b[2], b[1], b[0], b[3])
[docs]
def transpose_y(b: Bounds) -> Bounds:
"""Flips the min/max y values of a bounds"""
return (b[0], b[3], b[2], b[1])
[docs]
def no_transpose(b: Bounds) -> Bounds:
"""No-op transpose function"""
return b