summaryrefslogtreecommitdiff
path: root/gerbonara
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara')
-rw-r--r--gerbonara/apertures.py188
-rwxr-xr-xgerbonara/excellon.py4
-rw-r--r--gerbonara/graphic_objects.py2
3 files changed, 141 insertions, 53 deletions
diff --git a/gerbonara/apertures.py b/gerbonara/apertures.py
index 086d5b1..e22b702 100644
--- a/gerbonara/apertures.py
+++ b/gerbonara/apertures.py
@@ -10,23 +10,23 @@ 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),
+ 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),
+ 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)
+ return self._primitives(x, y, unit, polarity_dark)
-def strip_right(*args):
+def _strip_right(*args):
args = list(args)
while args and args[-1] is None:
args.pop()
return args
-def none_close(a, b):
+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:
@@ -35,24 +35,37 @@ def none_close(a, b):
return False
class Length:
+ """ Marker indicating that a dataclass field of an :py:class:`.Aperture` contains a physical length or coordinate
+ measured in the :py:class:`.Aperture`'s native unit from :py:attr:`.Aperture.unit`.
+ """
def __init__(self, obj_type):
self.type = obj_type
@dataclass
class Aperture:
+ """ Base class for all apertures. """
_ : KW_ONLY
+ #: :py:class:`gerbonara.utils.LengthUnit` used for all length fields of this aperture.
unit : str = None
+ #: GerberX2 attributes of this aperture. Note that this will only contain aperture attributes, not file attributes.
+ #: File attributes are stored in the :py:attr:`~.GerberFile.attrs` of the :py:class:`.GerberFile`.
attrs : dict = field(default_factory=dict)
- original_number : str = None
+ #: Aperture index this aperture had when it was read from the Gerber file. This field is purely informational since
+ #: apertures are de-duplicated and re-numbered when writing a Gerber file. For `D10`, this field would be `10`. When
+ #: you programmatically create a new aperture, you do not have to set this.
+ original_number : int = None
@property
def hole_shape(self):
- if hasattr(self, 'hole_rect_h') and self.hole_rect_h is not None:
+ """ Get shape of hole based on :py:attr:`hole_dia` and :py:attr:`hole_rect_h`: "rect" or "circle" or None. """
+ if getattr(self, 'hole_rect_h') is not None:
return 'rect'
- else:
+ elif getattr(self, 'hole_dia') is not None:
return 'circle'
+ else:
+ return None
- def params(self, unit=None):
+ def _params(self, unit=None):
out = []
for f in fields(self):
if f.kw_only:
@@ -66,24 +79,52 @@ class Aperture:
return out
def flash(self, x, y, unit=None, polarity_dark=True):
- return self.primitives(x, y, unit, polarity_dark)
+ """ Render this aperture into a ``list`` of :py:class:`.GraphicPrimitive` instances in the given unit. If no
+ unit is given, use this aperture's local unit.
+
+ :param float x: X coordinate of center of flash.
+ :param float y: Y coordinate of center of flash.
+ :param LengthUnit unit: Physical length unit to use for the returned primitives.
+ :param bool polarity_dark: Polarity of this flash. ``True`` renders this aperture as usual. ``False`` flips the polarity of all primitives.
+
+ :returns: Rendered graphic primitivees.
+ :rtype: list(:py:class:`.GraphicPrimitive`)
+ """
+ return self._primitives(x, y, unit, polarity_dark)
def equivalent_width(self, unit=None):
+ """ Get the width of a line interpolated using this aperture in the given :py:class:`~.LengthUnit`.
+
+ :rtype: float
+ """
raise ValueError('Non-circular aperture used in interpolation statement, line width is not properly defined.')
def to_gerber(self, settings=None):
+ """ Return the Gerber aperture definition for this aperture using the given :py:class:`.FileSettings`.
+
+ :rtype: str
+ """
# 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)
+ 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}'
+ return f'{actual_inst._gerber_shape_code},{params}'
else:
- return actual_inst.gerber_shape_code
+ return actual_inst._gerber_shape_code
+
+ def to_macro(self):
+ """ Convert this :py:class:`.Aperture` into an :py:class:`.ApertureMacro` inside an
+ :py:class:`.ApertureMacroInstance`.
+ """
+ raise NotImplementedError()
def __eq__(self, other):
+ """ Compare two apertures. Apertures are compared based on their Gerber representation. Two apertures are
+ considered equal if their Gerber aperture definitions are identical.
+ """
# We need to choose some unit here.
return hasattr(other, 'to_gerber') and self.to_gerber(MM) == other.to_gerber(MM)
@@ -95,39 +136,44 @@ class Aperture:
@dataclass(unsafe_hash=True)
class ExcellonTool(Aperture):
- gerber_shape_code = 'C'
- human_readable_shape = 'drill'
+ """ Special Aperture_ subclass for use in :py:class:`.ExcellonFile`. Similar to :py:class:`.CircleAperture`, but
+ does not have :py:attr:`.CircleAperture.hole_dia` or :py:attr:`.CircleAperture.hole_rect_h`, and has the additional
+ :py:attr:`plated` attribute.
+ """
+ _gerber_shape_code = 'C'
+ _human_readable_shape = 'drill'
+ #: float with diameter of this tool in :py:attr:`unit` units.
diameter : Length(float)
+ #: bool or ``None`` for "unknown", indicating whether this tool creates plated (``True``) or non-plated (``False``)
+ #: holes.
plated : bool = None
- depth_offset : Length(float) = 0
- def primitives(self, x, y, unit=None, polarity_dark=True):
+ 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
+ return 'C' + settings.write_excellon_value(self.diameter, self.unit)
def __eq__(self, other):
+ """ Compare two :py:class:`.ExcellonTool` instances. They are considered equal if their diameter and plating
+ match.
+ """
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))
+ 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}]>'
+ return f'<Excellon Tool d={self.diameter:.3f}{plated} [{self.unit}]>'
def equivalent_width(self, unit=MM):
return unit(self.diameter, self.unit)
+ # Internal use, for layer dilation.
def dilated(self, offset, unit=MM):
offset = unit(offset, self.unit)
return replace(self, diameter=self.diameter+2*offset)
@@ -136,22 +182,29 @@ class ExcellonTool(Aperture):
return self
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM))
+ return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM))
- def params(self, unit=None):
+ 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'
+ """ Besides flashing circles or rings, CircleApertures are used to set the width of a
+ :py:class:`~.graphic_objects.Line` or :py:class:`~.graphic_objects.Arc`.
+ """
+ _gerber_shape_code = 'C'
+ _human_readable_shape = 'circle'
+ #: float with diameter of the circle in :py:attr:`unit` units.
diameter : Length(float)
+ #: float with the hole diameter of this aperture in :py:attr:`unit` units. ``0`` for no hole.
hole_dia : Length(float) = None
+ #: float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
hole_rect_h : Length(float) = None
- rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber
+ # float with radians. This is only used for rectangular holes (as circles are rotationally symmetric).
+ rotation : float = 0
- def primitives(self, x, y, unit=None, polarity_dark=True):
+ 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):
@@ -173,10 +226,10 @@ class CircleAperture(Aperture):
return self.to_macro(self.rotation)
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM))
+ return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM))
- def params(self, unit=None):
- return strip_right(
+ 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))
@@ -184,15 +237,20 @@ class CircleAperture(Aperture):
@dataclass
class RectangleAperture(Aperture):
- gerber_shape_code = 'R'
- human_readable_shape = 'rect'
+ _gerber_shape_code = 'R'
+ _human_readable_shape = 'rect'
+ #: float with the width of the rectangle in :py:attr:`unit` units.
w : Length(float)
+ #: float with the height of the rectangle in :py:attr:`unit` units.
h : Length(float)
+ #: float with the hole diameter of this aperture in :py:attr:`unit` units. ``0`` for no hole.
hole_dia : Length(float) = None
+ #: float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
hole_rect_h : Length(float) = None
+ # Rotation in radians. This rotates both the aperture and the rectangular hole if it has one.
rotation : float = 0 # radians
- def primitives(self, x, y, unit=None, polarity_dark=True):
+ 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) ]
@@ -224,8 +282,8 @@ class RectangleAperture(Aperture):
MM(self.hole_rect_h, self.unit) or 0,
self.rotation])
- def params(self, unit=None):
- return strip_right(
+ 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),
@@ -234,15 +292,26 @@ class RectangleAperture(Aperture):
@dataclass
class ObroundAperture(Aperture):
- gerber_shape_code = 'O'
- human_readable_shape = 'obround'
+ """ Aperture whose shape is the convex hull of two circles of equal radii.
+
+ Obrounds are specified through width and height of their bounding rectangle.. The smaller one of these will be the
+ diameter of the obround's ends. If :py:attr:`w` is larger, the result will be a landscape obround. If :py:attr:`h`
+ is larger, it will be a portrait obround.
+ """
+ _gerber_shape_code = 'O'
+ _human_readable_shape = 'obround'
+ #: float with the width of the bounding rectangle of this obround in :py:attr:`unit` units.
w : Length(float)
+ #: float with the height of the bounding rectangle of this obround in :py:attr:`unit` units.
h : Length(float)
+ #: float with the hole diameter of this aperture in :py:attr:`unit` units. ``0`` for no hole.
hole_dia : Length(float) = None
+ #: float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
hole_rect_h : Length(float) = None
+ #: Rotation in radians. This rotates both the aperture and the rectangular hole if it has one.
rotation : float = 0
- def primitives(self, x, y, unit=None, polarity_dark=True):
+ 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) ]
@@ -273,8 +342,8 @@ class ObroundAperture(Aperture):
MM(inst.hole_rect_h, self.unit),
inst.rotation])
- def params(self, unit=None):
- return strip_right(
+ 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),
@@ -283,16 +352,24 @@ class ObroundAperture(Aperture):
@dataclass
class PolygonAperture(Aperture):
- gerber_shape_code = 'P'
+ """ Aperture whose shape is a regular n-sided polygon (e.g. pentagon, hexagon etc.). Note that this only supports
+ round holes.
+ """
+ _gerber_shape_code = 'P'
+ #: Diameter of circumscribing circle, i.e. the circle that all the polygon's corners lie on. In
+ #: :py:attr:`unit` units.
diameter : Length(float)
+ #: Number of corners of this polygon. Three for a triangle, four for a square, five for a pentagon etc.
n_vertices : int
+ #: Rotation in radians.
rotation : float = 0
+ #: float with the hole diameter of this aperture in :py:attr:`unit` units. ``0`` for no hole.
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):
+ 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) ]
@@ -309,7 +386,7 @@ class PolygonAperture(Aperture):
return self
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.polygon, self.params(MM))
+ 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
@@ -322,15 +399,26 @@ class PolygonAperture(Aperture):
@dataclass
class ApertureMacroInstance(Aperture):
+ """ One instance of an aperture macro. An aperture macro defined with an ``AM`` statement can be instantiated by
+ multiple ``AD`` aperture definition statements using different parameters. An :py:class:`.ApertureMacroInstance` is
+ one such binding of a macro to a particular set of parameters. Note that you still need an
+ :py:class:`.ApertureMacroInstance` even if your :py:class:`.ApertureMacro` has no parameters since an
+ :py:class:`.ApertureMacro` is not an :py:class:`.Aperture` by itself.
+ """
+ #: The :py:class:`.ApertureMacro` bound in this instance
macro : object
- parameters : [float]
+ #: The parameters to the :py:class:`.ApertureMacro`. All elements should be floats or ints. The first item in the
+ #: list is parameter ``$1``, the second is ``$2`` etc.
+ parameters : list
+ #: Aperture rotation in radians. When saving, a copy of the :py:class:`.ApertureMacro` is re-written with this
+ #: rotation.
rotation : float = 0
@property
- def gerber_shape_code(self):
+ def _gerber_shape_code(self):
return self.macro.name
- def primitives(self, x, y, unit=None, polarity_dark=True):
+ 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))
diff --git a/gerbonara/excellon.py b/gerbonara/excellon.py
index 96a78be..a6e9566 100755
--- a/gerbonara/excellon.py
+++ b/gerbonara/excellon.py
@@ -268,7 +268,7 @@ class ExcellonFile(CamFile):
# Build tool index
tool_map = { id(obj.tool): obj.tool for obj in self.objects }
- tools = sorted(tool_map.items(), key=lambda id_tool: (id_tool[1].plated, id_tool[1].diameter, id_tool[1].depth_offset))
+ tools = sorted(tool_map.items(), key=lambda id_tool: (id_tool[1].plated, id_tool[1].diameter))
tools = { tool_id: index for index, (tool_id, _tool) in enumerate(tools, start=1) }
# FIXME dedup tools
@@ -526,7 +526,7 @@ class ExcellonParser(object):
params = { m[0]: self.settings.parse_gerber_value(m[1:]) for m in re.findall('[BCFHSTZ][.0-9]+', match[2]) }
- self.tools[index] = ExcellonTool(diameter=params.get('C'), depth_offset=params.get('Z'), plated=self.is_plated,
+ self.tools[index] = ExcellonTool(diameter=params.get('C'), plated=self.is_plated,
unit=self.settings.unit)
if set(params.keys()) == set('TFSC'):
diff --git a/gerbonara/graphic_objects.py b/gerbonara/graphic_objects.py
index 99f990f..1ab03dd 100644
--- a/gerbonara/graphic_objects.py
+++ b/gerbonara/graphic_objects.py
@@ -37,7 +37,7 @@ class GraphicObject:
#: for features of an :py:class:`gerbonara.excellon.ExcellonFile`.
polarity_dark : bool = True
- #: :py:class:`gerbonara.utils.LengthUnit` used for all coordinate fields of this feature (such as `x` or `y`).
+ #: :py:class:`gerbonara.utils.LengthUnit` used for all coordinate fields of this object (such as ``x`` or ``y``).
unit : str = None