from __future__ import annotations
from collections.abc import Sequence
from enum import IntEnum
from typing import Self, overload, override
from jitx.feature import Silkscreen, Soldermask
from jitx.layerindex import Side
from jitx.shapes import Shape
from jitx.shapes.composites import Bounds, bounds_area, buffer_bounds
from jitx.shapes.shapely import ShapelyGeometry
import shapely
from . import SilkscreenSoldermaskClearanceMixin
from .. import ApplyToMixin, LandpatternGenerator, LineWidthMixin
from ..ipc import DensityLevelMixin
from ..package import PackageBodyMixin
from logging import getLogger
logger = getLogger(__name__)
[docs]
class SilkscreenLine(IntEnum):
"""Silkscreen Edge"""
Top = 1
Bottom = 2
Left = 4
Right = 8
Vertical = Left | Right
Horizontal = Top | Bottom
Perimeter = Horizontal | Vertical
XAxis = 16
YAxis = 32
[docs]
class SilkscreenLineGenerator:
__line: int = SilkscreenLine.Perimeter
__corner: float = 0
__line_offset: float = 0
[docs]
def silkscreen_line(self, line: SilkscreenLine | int, offset: float = 0):
"""Set the silkscreen edge to draw.
Args:
line: The silkscreen edge to use.
offset: Offset the line by this amount. Mainly for aesthetic
reasons. Positive is outwards.
Returns:
self for method chaining.
"""
if not 0 < line < 64:
raise ValueError("Invalid silkscreen edge")
self.__line = line
self.__line_offset = offset
return self
[docs]
def silkscreen_corner(self, corner: float = 0.15):
"""Set the silkscreen corner ratio"""
self.__corner = corner
return self
def _silkscreen_line(self, bounds: Bounds, line_width: float) -> Shape:
Top = SilkscreenLine.Top
Bottom = SilkscreenLine.Bottom
Left = SilkscreenLine.Left
Right = SilkscreenLine.Right
XAxis = SilkscreenLine.XAxis
YAxis = SilkscreenLine.YAxis
minx, miny, maxx, maxy = bounds
minx -= self.__line_offset
miny -= self.__line_offset
maxx += self.__line_offset
maxy += self.__line_offset
multiline = []
if self.__line & SilkscreenLine.Perimeter:
perimeter = (
((maxx, maxy), (minx, maxy)),
((minx, maxy), (minx, miny)),
((minx, miny), (maxx, miny)),
((maxx, miny), (maxx, maxy)),
)
pts = []
for idx, elem in enumerate((Top, Left, Bottom, Right)):
if self.__line & elem:
if not pts:
pts.extend(perimeter[idx])
else:
pts.append(perimeter[idx][1])
elif pts:
multiline.append(pts)
pts = []
if pts:
multiline.append(pts)
if self.__line & (XAxis | YAxis):
midx = (minx + maxx) / 2.0
midy = (miny + maxy) / 2.0
axes = (
((minx, midy), (maxx, midy)),
((midx, miny), (midx, maxy)),
)
for idx, elem in enumerate((XAxis, YAxis)):
if self.__line & elem:
multiline.append(axes[idx])
geom = shapely.MultiLineString(multiline).buffer(line_width / 2.0)
if self.__corner:
r = self.__corner * min(maxx, maxy)
geom = geom.union(
shapely.Polygon(
[
(minx + r, maxy),
(minx, maxy),
(minx, maxy - r),
]
)
)
return ShapelyGeometry(geom)
[docs]
class OutlineGenerator(SilkscreenLineGenerator):
__line_width: float | None = None
[docs]
def line_width(self, width: float):
"""Override the line width for this generator only. If not specified,
the line width from the landpattern generator is used."""
self.__line_width = width
return self
def _line_width(self, target: SilkscreenOutline) -> float:
return self.__line_width or target._line_width
[docs]
def make_bounds(self, target: SilkscreenOutline) -> Bounds:
"""Generate the silkscreen outline"""
raise NotImplementedError
[docs]
def make_shape(self, target: SilkscreenOutline) -> Shape:
return self._silkscreen_line(self.make_bounds(target), self._line_width(target))
[docs]
class SoldermaskBased(OutlineGenerator):
[docs]
@override
def make_bounds(self, target: SilkscreenOutline) -> Bounds:
line_width = self._line_width(target)
clearance = target._silkscreen_soldermask_clearance
sm_bounds = target._applies_to_bounds(Soldermask)
margin = clearance + line_width / 2.0
buffered_bounds = buffer_bounds(sm_bounds, margin)
if bounds_area(buffered_bounds) <= 0.0:
raise ValueError(
"Soldermask bounds are too small to construct soldermask-based"
+ f" silkscreen outline: {sm_bounds}"
)
return buffered_bounds
[docs]
class PackageBased(OutlineGenerator):
[docs]
@override
def make_bounds(self, target: SilkscreenOutline) -> Bounds:
pb = target._package_body()
dl = target._density_level
pb_bounds = pb.envelope(dl).to_shapely().bounds
if bounds_area(pb_bounds) <= 0.0:
raise ValueError(
"Package body envelope is too small to construct package-based"
+ f" silkscreen outline: {pb_bounds}"
)
return pb_bounds
[docs]
class SilkscreenOutline(
ApplyToMixin,
PackageBodyMixin,
SilkscreenSoldermaskClearanceMixin,
DensityLevelMixin,
LineWidthMixin,
LandpatternGenerator,
):
"""Silkscreen Outline"""
outline: Silkscreen | None = None
__generators: Sequence[OutlineGenerator] | None = (SoldermaskBased(),)
__side: Side = Side.Top
@overload
def silkscreen_outline(self, generator: None) -> Self: ...
@overload
def silkscreen_outline(
self,
generator: OutlineGenerator,
*fallback: OutlineGenerator,
on: Side = Side.Top,
) -> Self: ...
[docs]
def silkscreen_outline(
self,
generator: OutlineGenerator | None,
*fallback: OutlineGenerator,
on: Side = Side.Top,
) -> Self:
"""Add a silkscreen outline to the landpattern.
Args:
generator: The outline generator to use. If None, the outline will be
removed.
fallback: Additional outline generators to try if the first one
fails, e.g. if running out of space.
on: The side of the board to apply the outline to.
"""
if generator is None:
self.__generators = None
else:
self.__generators = (generator,) + fallback
self.__side = on
return self
@override
def _build(self):
self.outline = None
super()._build()
@override
def _build_decorate(self):
super()._build_decorate()
if self.__generators:
margin = self._silkscreen_soldermask_clearance
masked = ShapelyGeometry(
shapely.unary_union(
[sh.to_shapely().g for sh in self._applies_to_shapes(Soldermask)]
)
).buffer(margin)
generators = iter(self.__generators)
gen = next(generators, None)
while gen:
try:
self.outline = Silkscreen(
gen.make_shape(self).to_shapely().difference(masked),
side=self.__side,
)
return
except ValueError:
old_gen = gen
gen = next(generators, None)
if gen is None:
raise
logger.info(
f"Failed to construct silkscreen outline with {old_gen}, trying {gen}"
)
[docs]
class InterstitialOutline(OutlineGenerator, SilkscreenSoldermaskClearanceMixin):
"""Interstitial Silkscreen Outline Generator
This class constructs a rectangular outline surrounding the interstitial
(interior) region of a landpattern. This typically corresponds to the area
where the package body of the component rests. The interstitial region is
estimated based on the inside edges of the pads, this works best for
two-pin, dual-column, and quad-column packages.
This outline style is commonly used for packages such as QFP, SOIC, SSOP.
It is also commonly used for 2-pin SMT and through-hole components.
>>> class MyComponent(Component):
... landpattern = LandpatternWithOutline(
... ).silkscreen_outline(InterstitialOutline())
"""
@overload
def __init__(self): ...
@overload
def __init__(self, *, vertical: bool): ...
@overload
def __init__(self, *, horizontal: bool): ...
def __init__(self, *, vertical: bool = False, horizontal: bool = False):
"""
Initialize the interstitial outline generator, optionally extending the
outline to the vertical or horizontal bounds. This is useful for column
packages such as SOIC, where the outline would otherwise stop short of
the edge at the inside of the end pads.
Args:
vertical: If True, the outline will be extended to the vertical bounds.
horizontal: If True, the outline will be horizontal to the horizontal bounds.
"""
super().__init__()
self.__vertical = vertical
self.__horizontal = horizontal
[docs]
@override
def make_bounds(self, target: SilkscreenOutline) -> Bounds:
line_width = target._line_width
clearance = self._silkscreen_soldermask_clearance
inf = float("inf")
hibounds = [inf, inf, -inf, -inf]
lobounds = [inf, inf, -inf, -inf]
for shape in target._applies_to_shapes(Soldermask):
lox, loy, hix, hiy = shape.to_shapely().bounds
hibounds[0] = min(hibounds[0], hix)
hibounds[1] = min(hibounds[1], hiy)
hibounds[2] = max(hibounds[2], hix)
hibounds[3] = max(hibounds[3], hiy)
lobounds[0] = min(lobounds[0], lox)
lobounds[1] = min(lobounds[1], loy)
lobounds[2] = max(lobounds[2], lox)
lobounds[3] = max(lobounds[3], loy)
margin = clearance + line_width / 2.0
if self.__vertical:
bounds = (hibounds[0], lobounds[1], lobounds[2], hibounds[3])
adjust = -margin, 0
elif self.__horizontal:
bounds = (lobounds[0], hibounds[1], hibounds[2], lobounds[3])
adjust = 0, -margin
else:
bounds = (hibounds[0], hibounds[1], lobounds[2], lobounds[3])
adjust = -margin, -margin
buffered_bounds = buffer_bounds(bounds, adjust)
if bounds_area(buffered_bounds) <= 0.0:
raise ValueError(
"Soldermask bounds are too small to construct an interstitial"
+ f" soldermask-based silkscreen outline: {bounds}"
)
return buffered_bounds