More doc
This commit is contained in:
parent
7473e471dc
commit
0fa713af4b
6 changed files with 183 additions and 176 deletions
|
|
@ -1,124 +1,17 @@
|
|||
Apertures in Gerbonara
|
||||
======================
|
||||
|
||||
Gerbonara maps all standard Gerber apertures to subclasses of the Aperture_ class. These subclasses: CircleAperture_,
|
||||
RectangleAperture_, ObroundAperture_ and PolygonAperture_. Aperture macro instantiations get mapped to
|
||||
ApertureMacroInstance_ (also an Aperture_ subclass).
|
||||
Gerbonara maps all standard Gerber apertures to subclasses of the :py:class:`.Aperture` class. These subclasses:
|
||||
:py:class:`.CircleAperture`, :py:class:`.RectangleAperture`, :py:class:`.ObroundAperture` and
|
||||
:py:class:`.PolygonAperture`. :doc:`Aperture macro<aperture-macros>` instantiations get mapped to
|
||||
:py:class:`.ApertureMacroInstance` (also an :py:class:`Aperture` subclass). The basic aperture shapes each support a
|
||||
central hole through their :py:class:`~.CircleAperture.hole_dia` attribute. This "hole" is just a cut-out in the
|
||||
aperture itself, and does not imply an actual drilled hole in the board. Drilled holes in the board are specified
|
||||
completely separately through an :py:class:`.ExcellonFile`.
|
||||
|
||||
All Aperture_ subclasses have these common attributes:
|
||||
|
||||
|
||||
`hole_dia`
|
||||
float with diameter of hole. 0 for no hole.
|
||||
|
||||
`hole_rect_h`
|
||||
float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
|
||||
|
||||
`unit`
|
||||
LengthUnit_ for all of this aperture's fields
|
||||
|
||||
`attrs`
|
||||
GerberX2 attributes of this aperture. Note that this will only contain aperture attributes, not file attributes.
|
||||
File attributes are stored in the `attrs` of GerberFile_.
|
||||
|
||||
`original_number`
|
||||
int of 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`.
|
||||
If you programmatically create a new aperture, you do not have to set this.
|
||||
|
||||
`rotation`
|
||||
Aperture rotation in radians counter-clockwise. This field is not part of the Gerber standard. Standard rectangle
|
||||
and obround apertures do not support rotation. Gerbonara converts rotated apertures into aperture macros during
|
||||
Gerber export as necessary.
|
||||
|
||||
CircleAperture
|
||||
--------------
|
||||
|
||||
This is the only one valid for use in Line_ or Arc_.
|
||||
|
||||
Attributes:
|
||||
|
||||
Common attributes:
|
||||
`hole_dia`, `hole_rect_h`, `unit`, `attrs`, and `original_number`. `rotation` is present but has no effect in
|
||||
CircleAperture_.
|
||||
|
||||
`diameter`
|
||||
float with diameter of aperture in the unit from the aperture's `unit` field.
|
||||
|
||||
RectangleAperture
|
||||
-----------------
|
||||
|
||||
Common attributes:
|
||||
`hole_dia`, `hole_rect_h`, `unit`, `attrs`, `original_number`, and `rotation`
|
||||
|
||||
`w`, `h`
|
||||
floats with width or height of rectangle in units from the aperture's `unit` field.
|
||||
|
||||
ObroundAperture
|
||||
---------------
|
||||
|
||||
Aperture whose shape is the convex hull of two circles of equal radii.
|
||||
|
||||
Common attributes:
|
||||
`hole_dia`, `hole_rect_h`, `unit`, `attrs`, `original_number`, and `rotation`
|
||||
|
||||
`w`, `h`
|
||||
floats with width and height of bounding box of obround. The smaller one of these will be the diameter of the
|
||||
obround's ends. If `w` is larger, the result will be a landscape obround. If `h` is larger, it will be a portrait
|
||||
obround.
|
||||
|
||||
PolygonAperture
|
||||
---------------
|
||||
|
||||
Aperture whose shape is a regular n-sided polygon (e.g. pentagon, hexagon etc.).
|
||||
|
||||
|
||||
Common attributes:
|
||||
`hole_dia`, `unit`, `attrs`, `original_number`, and `rotation`. `hole_rect_h` is not supported in PolygonAperture_
|
||||
since the Gerber spec does not list it.
|
||||
|
||||
`diameter`
|
||||
float with diameter of circumscribing circle, i.e. the circle that all the polygon's corners lie on.
|
||||
|
||||
`n_vertices`
|
||||
int with number of corners of this polygon. Three for a triangle, four for a square, five for a pentagon etc.
|
||||
|
||||
ApertureMacroInstance
|
||||
---------------------
|
||||
|
||||
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 ApertureMacroInstance_ is one such binding of a macro to a
|
||||
particular set of parameters. Note that you still need an ApertureMacroInstance_ even if your ApertureMacro_ has no
|
||||
parameters since an ApertureMacro_ is not an Aperture_ by itself.
|
||||
|
||||
Attributes:
|
||||
|
||||
Common attributes:
|
||||
`unit`, `attrs`, `original_number`, and `rotation`. ApertureMacroInstance_ does not support `hole_dia` or
|
||||
`hole_rect_h`. `rotation` is handled by re-writing the ApertureMacro_ during export.
|
||||
|
||||
`macro`
|
||||
The ApertureMacro_ that is bound here
|
||||
|
||||
`parameters`
|
||||
list of ints or floats with the parameters for this macro. The first element is `$1`, the second is `$2` etc.
|
||||
|
||||
ExcellonTool
|
||||
------------
|
||||
|
||||
Special Aperture_ subclass for use in ExcellonFile_. Similar to CircleAperture_, but does not have `hole_dia` or
|
||||
`hole_rect_h`, and has additional `plated` and `depth_offset` attributes.
|
||||
|
||||
|
||||
Common attributes:
|
||||
`unit`, `original_number`
|
||||
|
||||
`plated`
|
||||
bool or None. True if this hole/slot is copper-plated, False if not, and None if it is undefined or unknown.
|
||||
|
||||
`depth_offset`
|
||||
float with Excellon depth offset for this hole or slot. If the fab supports this, this can be used to create
|
||||
features that do not go all the way through the board.
|
||||
Gerbonara is able to rotate any aperture. The Gerber standard does not support rotation for standard apertures in any
|
||||
widespread way, so Gerbolyze handles rotation by converting rotated standard apertures into aperture macros during
|
||||
export as necessary.
|
||||
|
||||
Aperture generalization
|
||||
-----------------------
|
||||
|
|
@ -132,7 +25,29 @@ For polygons, we simply change the angle parameter. However, for rectangles and
|
|||
supports a rotation parameter. The only way to rotate these is to convert them to an aperture macro, then rotate that.
|
||||
|
||||
This behavior of using aperture macros for general rotated rectangles is common behavior among CAD tools. Gerbonara adds
|
||||
a non-standard `rotation` attribute to all apertures except CircleAperture_ and transparently converts rotated instances
|
||||
to the appropriate ApertureMacroInstance_ objects while it writes out the file. Be aware that this may mean that an
|
||||
object that in memory has a RectangleAperture_ might end up with an aperture macro instance in the output Gerber file.
|
||||
a non-standard :py:attr:`.RectangleAperture.rotation` attribute to all apertures except :py:class:`.CircleAperture` and
|
||||
transparently converts rotated instances to the appropriate :py:class:`.ApertureMacroInstance` objects while it writes
|
||||
out the file. Be aware that this may mean that an object that in memory has a :py:class:`.RectangleAperture` might end
|
||||
up with an aperture macro instance in the output Gerber file.
|
||||
|
||||
Aperture classes
|
||||
----------------
|
||||
|
||||
.. autoclass:: gerbonara.apertures.Aperture
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.apertures.CircleAperture
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.apertures.RectangleAperture
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.apertures.ObroundAperture
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.apertures.PolygonAperture
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.apertures.ApertureMacroInstance
|
||||
:members:
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ Gerbonara currently supports three file types: RS-274-X Gerber as `specified by
|
|||
|
||||
Usually, a PCB is sent to a manufacturer as a bundle of several of these files. Such a bundle of files (each of which is
|
||||
either a :py:class:`.GerberFile` or an :py:class:`.ExcellonFile`) is represented by :py:class:`.LayerStack`.
|
||||
:py:class:`.LayerStack` contains logic to automatcally
|
||||
recognize a wide variety of CAD tools from file name and syntactic hints, and can automatically match all files in a
|
||||
folder to their appropriate layers.
|
||||
:py:class:`.LayerStack` contains logic to automatcally recognize a wide variety of CAD tools from file name and
|
||||
syntactic hints, and can automatically match all files in a folder to their appropriate layers.
|
||||
|
||||
.. autoclass:: gerbonara.layers.LayerStack
|
||||
:members:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
Utilities
|
||||
=========
|
||||
|
||||
.. autoclass:: gerbonara.utils.LengthUnit
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.cam.FileSettings
|
||||
:members:
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue