diff options
Diffstat (limited to 'gerbonara/gerber/apertures.py')
-rw-r--r-- | gerbonara/gerber/apertures.py | 360 |
1 files changed, 0 insertions, 360 deletions
diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py deleted file mode 100644 index ab62077..0000000 --- a/gerbonara/gerber/apertures.py +++ /dev/null @@ -1,360 +0,0 @@ - -import math -from dataclasses import dataclass, replace, field, fields, InitVar, KW_ONLY - -from .aperture_macros.parse import GenericMacros -from .utils import MM, Inch - -from . import graphic_primitives as gp - - -def _flash_hole(self, x, y, unit=None, polarity_dark=True): - if getattr(self, 'hole_rect_h', None) is not None: - return [*self.primitives(x, y, unit, polarity_dark), - gp.Rectangle((x, y), - (self.unit.convert_to(unit, self.hole_dia), self.unit.convert_to(unit, self.hole_rect_h)), - rotation=self.rotation, polarity_dark=(not polarity_dark))] - elif self.hole_dia is not None: - return [*self.primitives(x, y, unit, polarity_dark), - gp.Circle(x, y, self.unit.convert_to(unit, self.hole_dia/2), polarity_dark=(not polarity_dark))] - else: - return self.primitives(x, y, unit, polarity_dark) - -def strip_right(*args): - args = list(args) - while args and args[-1] is None: - args.pop() - return args - -def none_close(a, b): - if a is None and b is None: - return True - elif a is not None and b is not None: - return math.isclose(a, b) - else: - return False - -class Length: - def __init__(self, obj_type): - self.type = obj_type - -@dataclass -class Aperture: - _ : KW_ONLY - unit : str = None - attrs : dict = field(default_factory=dict) - original_number : str = None - - @property - def hole_shape(self): - if hasattr(self, 'hole_rect_h') and self.hole_rect_h is not None: - return 'rect' - else: - return 'circle' - - def params(self, unit=None): - out = [] - for f in fields(self): - if f.kw_only: - continue - - val = getattr(self, f.name) - if isinstance(f.type, Length): - val = self.unit.convert_to(unit, val) - out.append(val) - - return out - - def flash(self, x, y, unit=None, polarity_dark=True): - return self.primitives(x, y, unit, polarity_dark) - - def equivalent_width(self, unit=None): - raise ValueError('Non-circular aperture used in interpolation statement, line width is not properly defined.') - - def to_gerber(self, settings=None): - # 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. - unit = settings.unit if settings else None - actual_inst = self._rotated() - params = 'X'.join(f'{float(par):.4}' for par in actual_inst.params(unit) if par is not None) - if params: - return f'{actual_inst.gerber_shape_code},{params}' - else: - return actual_inst.gerber_shape_code - - def __eq__(self, other): - # We need to choose some unit here. - return hasattr(other, 'to_gerber') and self.to_gerber(MM) == other.to_gerber(MM) - - 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(unsafe_hash=True) -class ExcellonTool(Aperture): - human_readable_shape = 'drill' - diameter : Length(float) - plated : bool = None - depth_offset : Length(float) = 0 - - def primitives(self, x, y, unit=None, polarity_dark=True): - return [ gp.Circle(x, y, self.unit.convert_to(unit, self.diameter/2), polarity_dark=polarity_dark) ] - - def to_xnc(self, settings): - z_off = 'Z' + settings.write_excellon_value(self.depth_offset, self.unit) if self.depth_offset is not None else '' - return 'C' + settings.write_excellon_value(self.diameter, self.unit) + z_off - - def __eq__(self, other): - if not isinstance(other, ExcellonTool): - return False - - if not self.plated == other.plated: - return False - - if not none_close(self.depth_offset, self.unit(other.depth_offset, other.unit)): - return False - - return none_close(self.diameter, self.unit(other.diameter, other.unit)) - - def __str__(self): - plated = '' if self.plated is None else (' plated' if self.plated else ' non-plated') - z_off = '' if self.depth_offset is None else f' z_offset={self.depth_offset}' - return f'<Excellon Tool d={self.diameter:.3f}{plated}{z_off} [{self.unit}]>' - - def equivalent_width(self, unit=MM): - return unit(self.diameter, self.unit) - - def dilated(self, offset, unit=MM): - offset = unit(offset, self.unit) - return replace(self, diameter=self.diameter+2*offset) - - def _rotated(self): - return self - - def to_macro(self): - return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM)) - - def params(self, unit=None): - return [self.unit.convert_to(unit, self.diameter)] - - -@dataclass -class CircleAperture(Aperture): - gerber_shape_code = 'C' - human_readable_shape = 'circle' - diameter : Length(float) - hole_dia : Length(float) = None - hole_rect_h : Length(float) = None - rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber - - def primitives(self, x, y, unit=None, polarity_dark=True): - return [ gp.Circle(x, y, self.unit.convert_to(unit, self.diameter/2), polarity_dark=polarity_dark) ] - - def __str__(self): - return f'<circle aperture d={self.diameter:.3} [{self.unit}]>' - - flash = _flash_hole - - def equivalent_width(self, unit=None): - return self.unit.convert_to(unit, self.diameter) - - def dilated(self, offset, unit=MM): - offset = self.unit(offset, unit) - return replace(self, diameter=self.diameter+2*offset, hole_dia=None, hole_rect_h=None) - - def _rotated(self): - if math.isclose(self.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(unit=MM)) - - def params(self, unit=None): - return strip_right( - self.unit.convert_to(unit, self.diameter), - self.unit.convert_to(unit, self.hole_dia), - self.unit.convert_to(unit, self.hole_rect_h)) - - -@dataclass -class RectangleAperture(Aperture): - gerber_shape_code = 'R' - human_readable_shape = 'rect' - w : Length(float) - h : Length(float) - hole_dia : Length(float) = None - hole_rect_h : Length(float) = None - rotation : float = 0 # radians - - def primitives(self, x, y, unit=None, polarity_dark=True): - return [ gp.Rectangle(x, y, self.unit.convert_to(unit, self.w), self.unit.convert_to(unit, self.h), - rotation=self.rotation, polarity_dark=polarity_dark) ] - - def __str__(self): - return f'<rect aperture {self.w:.3}x{self.h:.3} [{self.unit}]>' - - flash = _flash_hole - - def equivalent_width(self, unit=None): - return self.unit.convert_to(unit, math.sqrt(self.w**2 + self.h**2)) - - def dilated(self, offset, unit=MM): - offset = self.unit(offset, unit) - return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None) - - 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(), rotation=0) - else: # odd angle - return self.to_macro() - - def to_macro(self): - return ApertureMacroInstance(GenericMacros.rect, - [MM(self.w, self.unit), - MM(self.h, self.unit), - MM(self.hole_dia, self.unit) or 0, - MM(self.hole_rect_h, self.unit) or 0, - self.rotation]) - - def params(self, unit=None): - return strip_right( - self.unit.convert_to(unit, self.w), - self.unit.convert_to(unit, self.h), - self.unit.convert_to(unit, self.hole_dia), - self.unit.convert_to(unit, self.hole_rect_h)) - - -@dataclass -class ObroundAperture(Aperture): - gerber_shape_code = 'O' - human_readable_shape = 'obround' - w : Length(float) - h : Length(float) - hole_dia : Length(float) = None - hole_rect_h : Length(float) = None - rotation : float = 0 - - def primitives(self, x, y, unit=None, polarity_dark=True): - return [ gp.Obround(x, y, self.unit.convert_to(unit, self.w), self.unit.convert_to(unit, self.h), - rotation=self.rotation, polarity_dark=polarity_dark) ] - - def __str__(self): - return f'<obround aperture {self.w:.3}x{self.h:.3} [{self.unit}]>' - - flash = _flash_hole - - def dilated(self, offset, unit=MM): - offset = self.unit(offset, unit) - return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None) - - 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(), rotation=0) - else: - return self.to_macro() - - def to_macro(self): - # 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), rotation=self.rotation-90) - return ApertureMacroInstance(GenericMacros.obround, - [MM(inst.w, self.unit), - MM(ints.h, self.unit), - MM(inst.hole_dia, self.unit), - MM(inst.hole_rect_h, self.unit), - inst.rotation]) - - def params(self, unit=None): - return strip_right( - self.unit.convert_to(unit, self.w), - self.unit.convert_to(unit, self.h), - self.unit.convert_to(unit, self.hole_dia), - self.unit.convert_to(unit, self.hole_rect_h)) - - -@dataclass -class PolygonAperture(Aperture): - gerber_shape_code = 'P' - diameter : Length(float) - n_vertices : int - rotation : float = 0 - hole_dia : Length(float) = None - - def __post_init__(self): - self.n_vertices = int(self.n_vertices) - - def primitives(self, x, y, unit=None, polarity_dark=True): - return [ gp.RegularPolygon(x, y, self.unit.convert_to(unit, self.diameter)/2, self.n_vertices, - rotation=self.rotation, polarity_dark=polarity_dark) ] - - def __str__(self): - return f'<{self.n_vertices}-gon aperture d={self.diameter:.3} [{self.unit}]>' - - def dilated(self, offset, unit=MM): - offset = self.unit(offset, unit) - return replace(self, diameter=self.diameter+2*offset, hole_dia=None) - - flash = _flash_hole - - def _rotated(self): - return self - - def to_macro(self): - return ApertureMacroInstance(GenericMacros.polygon, self.params(MM)) - - def params(self, unit=None): - rotation = self.rotation % (2*math.pi / self.n_vertices) if self.rotation is not None else None - if self.hole_dia is not None: - return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation, self.unit.convert_to(unit, self.hole_dia) - elif rotation is not None and not math.isclose(rotation, 0): - return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation - else: - return self.unit.convert_to(unit, self.diameter), self.n_vertices - -@dataclass -class ApertureMacroInstance(Aperture): - macro : object - parameters : [float] - rotation : float = 0 - - @property - def gerber_shape_code(self): - return self.macro.name - - def primitives(self, x, y, unit=None, polarity_dark=True): - out = list(self.macro.to_graphic_primitives( - offset=(x, y), rotation=self.rotation, - parameters=self.parameters, unit=unit, polarity_dark=polarity_dark)) - return out - - def dilated(self, offset, unit=MM): - return replace(self, macro=self.macro.dilated(offset, unit)) - - def _rotated(self): - if math.isclose(self.rotation % (2*math.pi), 0): - return self - else: - return self.to_macro() - - def to_macro(self): - return replace(self, macro=self.macro.rotated(self.rotation), rotation=0) - - 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 - - def params(self, unit=None): - # We ignore "unit" here as we convert the actual macro, not this instantiation. - # We do this because here we do not have information about which parameter has which physical units. - return tuple(self.parameters) - - |