diff options
Diffstat (limited to 'gerbonara/gerber/apertures.py')
-rw-r--r-- | gerbonara/gerber/apertures.py | 164 |
1 files changed, 143 insertions, 21 deletions
diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py index aa2764e..2c03a37 100644 --- a/gerbonara/gerber/apertures.py +++ b/gerbonara/gerber/apertures.py @@ -1,11 +1,13 @@ -from dataclasses import dataclass +import math +from dataclasses import dataclass, replace +from aperture_macros.parse import GenericMacros -from primitives import Primitive +import graphic_primitives as gp def _flash_hole(self, x, y): if self.hole_rect_h is not None: - return self.primitives(x, y), Rectangle((x, y), (self.hole_dia, self.hole_rect_h), polarity_dark=False) + return self.primitives(x, y), Rectangle((x, y), (self.hole_dia, self.hole_rect_h), rotation=self.rotation, polarity_dark=False) else: return self.primitives(x, y), Circle((x, y), self.hole_dia, polarity_dark=False) @@ -21,65 +23,185 @@ class Aperture: def hole_size(self): return (self.hole_dia, self.hole_rect_h) + @property + def params(self): + return dataclasses.astuple(self) + def flash(self, x, y): return self.primitives(x, y) + @parameter + def equivalent_width(self): + raise ValueError('Non-circular aperture used in interpolation statement, line width is not properly defined.') + + def to_gerber(self): + # Hack: The standard aperture shapes C, R, O do not have a rotation parameter. To make this API easier to use, + # we emulate this parameter. Our circle, rectangle and oblong classes below have a rotation parameter. Only at + # export time during to_gerber, this parameter is evaluated. + actual_inst = self._rotated() + params = 'X'.join(f'{par:.4}' for par in actual_inst.params) + return f'{actual_inst.aperture.gerber_shape_code},{params}' -@dataclass -class ApertureCircle(Aperture): + def __eq__(self, other): + return hasattr(other, to_gerber) and self.to_gerber() == other.to_gerber() + + def _rotate_hole_90(self): + if self.hole_rect_h is None: + return {'hole_dia': self.hole_dia, 'hole_rect_h': None} + else: + return {'hole_dia': self.hole_rect_h, 'hole_rect_h': self.hole_dia} + + +@dataclass(frozen=True) +class CircleAperture(Aperture): + gerber_shape_code = 'C' + human_readable_shape = 'circle' diameter : float hole_dia : float = 0 hole_rect_h : float = None + rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber - def primitives(self, x, y): - return Circle((x, y), self.diameter, polarity_dark=True), + def primitives(self, x, y, rotation): + return [ gp.Circle(x, y, self.diameter/2) ] + + def __str__(self): + return f'<circle aperture d={self.diameter:.3}>' flash = _flash_hole + @parameter + def equivalent_width(self): + return self.diameter -@dataclass -class ApertureRectangle(Aperture): + def rotated(self): + if math.isclose(rotation % (2*math.pi), 0) or self.hole_rect_h is None: + return self + else: + return self.to_macro(self.rotation) + + def to_macro(self): + return ApertureMacroInstance(GenericMacros.circle, *self.params) + + +@dataclass(frozen=True) +class RectangleAperture(Aperture): + gerber_shape_code = 'R' + human_readable_shape = 'rect' w : float h : float hole_dia : float = 0 hole_rect_h : float = None + rotation : float = 0 # radians def primitives(self, x, y): - return Rectangle((x, y), (self.w, self.h), polarity_dark=True), + return [ gp.Rectangle(x, y, self.w, self.h, rotation=self.rotation) ] + + def __str__(self): + return f'<rect aperture {self.w:.3}x{self.h:.3}>' flash = _flash_hole + @parameter + def equivalent_width(self): + return math.sqrt(self.w**2 + self.h**2) + + def _rotated(self): + if math.isclose(self.rotation % math.pi, 0): + return self + elif math.isclose(self.rotation % math.pi, math.pi/2): + return replace(self, w=self.h, h=self.w, **self._rotate_hole_90()) + else: # odd angle + return self.to_macro() + + def to_macro(self): + return ApertureMacroInstance(GenericMacros.rect, *self.params) + -@dataclass -class ApertureObround(Aperture): +@dataclass(frozen=True) +class ObroundAperture(Aperture): + gerber_shape_code = 'O' + human_readable_shape = 'obround' w : float h : float hole_dia : float = 0 hole_rect_h : float = None + rotation : float = 0 def primitives(self, x, y): - return Obround((x, y), self.w, self.h, polarity_dark=True) + return [ gp.Obround(x, y, self.w, self.h, rotation=self.rotation) ] + + def __str__(self): + return f'<obround aperture {self.w:.3}x{self.h:.3}>' flash = _flash_hole + def _rotated(self): + if math.isclose(self.rotation % math.pi, 0): + return self + elif math.isclose(self.rotation % math.pi, math.pi/2): + return replace(self, w=self.h, h=self.w, **self._rotate_hole_90()) + else: + return self.to_macro() + + def to_macro(self, rotation:'radians'=0): + # generic macro only supports w > h so flip x/y if h > w + inst = self if self.w > self.h else replace(self, w=self.h, h=self.w, **_rotate_hole_90(self)) + return ApertureMacroInstance(GenericMacros.obround, *inst.params) + -@dataclass -class AperturePolygon(Aperture): +@dataclass(frozen=True) +class PolygonAperture(Aperture): + gerber_shape_code = 'P' diameter : float n_vertices : int + rotation : float = 0 hole_dia : float = 0 - hole_rect_h : float = None def primitives(self, x, y): - return Polygon((x, y), diameter, n_vertices, rotation, polarity_dark=True), + return [ gp.RegularPolygon(x, y, diameter, n_vertices, rotation=self.rotation) ] + + def __str__(self): + return f'<{self.n_vertices}-gon aperture d={self.diameter:.3}' flash = _flash_hole -class MacroAperture(Aperture): - parameters : [float] - self.macro : ApertureMacro + def _rotated(self): + self.rotation %= (2*math.pi / self.n_vertices) + return self + + def to_macro(self): + return ApertureMacroInstance(GenericMacros.polygon, *self.params) + + +class ApertureMacroInstance(Aperture): + params : [float] + rotation : float = 0 + + def __init__(self, macro, *parameters): + self.params = parameters + self._primitives = macro.to_graphic_primitives(parameters) + self.macro = macro + + @property + def gerber_shape_code(self): + return self.macro.name def primitives(self, x, y): - return self.macro.execute(x, y, self.parameters) + # FIXME return graphical primitives not macro primitives here + return [ primitive.with_offset(x, y).rotated(self.rotation, cx=0, cy=0) for primitive in self._primitives ] + + def _rotated(self): + if math.isclose(self.rotation % (2*math.pi), 0): + return self + else: + return self.to_macro() + + def to_macro(self): + return type(self)(self.macro.rotated(self.rotation), self.params) + + def __eq__(self, other): + return hasattr(other, 'macro') and self.macro == other.macro and \ + hasattr(other, 'params') and self.params == other.params and \ + hasattr(other, 'rotation') and self.rotation == other.rotation |