From 1aaac3936f4d84690b19fd6a40284af35c5b970c Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 25 Feb 2023 19:43:54 +0100 Subject: Still more doc --- gerbonara/cam.py | 2 +- gerbonara/layers.py | 65 ++++++++++++++++++++++++++++++++++++++++++++--------- gerbonara/rs274x.py | 31 +++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 14 deletions(-) (limited to 'gerbonara') diff --git a/gerbonara/cam.py b/gerbonara/cam.py index 59c4969..9819087 100644 --- a/gerbonara/cam.py +++ b/gerbonara/cam.py @@ -357,7 +357,7 @@ class CamFile: def merge(self, other): """ Merge ``other`` into ``self``, i.e. add all objects that are in ``other`` to ``self``. This resets :py:attr:`.import_settings` and :py:attr:`~.CamFile.generator`. Units and other file-specific settings are - automatically handled. + handled automatically. """ raise NotImplementedError() diff --git a/gerbonara/layers.py b/gerbonara/layers.py index 34d3f0c..c298ffc 100644 --- a/gerbonara/layers.py +++ b/gerbonara/layers.py @@ -247,9 +247,10 @@ class LayerStack: """ :py:class:`LayerStack` represents a set of Gerber files that describe different layers of the same board. :ivar graphic_layers: :py:obj:`dict` mapping :py:obj:`(side, use)` tuples to the Gerber layers of the board. - :py:obj:`side` can be one of :py:obj:`"top"` or :py:obj:`"bottom"`, or a numbered internal - layer such as :py:obj:`"inner2"`. :py:obj:`use` can be one of :py:obj:`"silk", :py:obj:`mask`, - :py:obj:`paste` or :py:obj:`copper`. For internal layers, only :py:obj:`copper` is valid. + :py:obj:`side` can be one of :py:obj:`"top"`, :py:obj:`"bottom"`, :py:obj:`"mechanical"`, or a + numbered internal layer such as :py:obj:`"inner2"`. :py:obj:`use` can be one of + :py:obj:`"silk", :py:obj:`mask`, :py:obj:`paste` or :py:obj:`copper`. For internal layers, + only :py:obj:`copper` is valid. :ivar board_name: Name of this board as parse from the input filenames, as a :py:obj:`str`. You can overwrite this attribute with a different name, which will then be used during saving with the built-in file naming rules. @@ -664,10 +665,10 @@ class LayerStack: color scheme. When given, must be a dict mapping semantic :py:obj:`"side use"` layer names such as :py:obj:`"top copper"` to a HTML-like hex color code such as :py:obj:`#ff00ea`. Transparency is supported through 8-digit color codes. When 8 digits are given, the last two digits are used - as the layer's alpha channel. Valid side values in the layer name strings are :py:obj:`"top"` and - :py:obj:`"bottom"` as well as :py:obj:`"inner1"`, :py:obj:`"inner2"` etc. for internal layers. - Valid use values are :py:obj:`"mask"`, :py:obj:`"silk"`, :py:obj:`"paste"`, and - :py:obj:`"copper"`. For internal layers, only :py:obj:`"copper"` is valid. + as the layer's alpha channel. Valid side values in the layer name strings are :py:obj:`"top"`, + :py:obj:`"bottom"`, and :py:obj:`"mechanical"` as well as :py:obj:`"inner1"`, :py:obj:`"inner2"` + etc. for internal layers. Valid use values are :py:obj:`"mask"`, :py:obj:`"silk"`, + :py:obj:`"paste"`, and :py:obj:`"copper"`. For internal layers, only :py:obj:`"copper"` is valid. :rtype: :py:obj:`str` """ if colors is None: @@ -798,6 +799,9 @@ class LayerStack: layer.scale(factor) def merge_drill_layers(self): + """ Merge all drill layers of this board into a single drill layer containing all objetcs. You can access this + drill layer under the :py:attr:`.LayerStack.drill_unknown` attribute. The original layers are removed from the + board. """ target = ExcellonFile(comments=['Drill files merged by gerbonara']) for layer in self.drill_layers: @@ -810,6 +814,9 @@ class LayerStack: self.drill_unknown = target def normalize_drill_layers(self): + """ Take everything from all drill layers of this board, and sort it into three new drill layers: One with all + non-plated objects, one with all plated objects, and one for all leftover objects with unknown plating. This + method replaces the board's drill layers with these three sorted ones. """ # TODO: maybe also separate into drill and route? drill_pth, drill_npth, drill_aux = [], [], [] @@ -848,6 +855,8 @@ class LayerStack: @property def drill_layers(self): + """ Return all of this board's drill layers as a list. Returns an empty list if the board does not have any + drill layers. """ if self._drill_layers: return self._drill_layers if self.drill_pth or self.drill_npth or self.drill_unknown: @@ -890,6 +899,8 @@ class LayerStack: @property def copper_layers(self): + """ Return all copper layers of this board as a list. Returns an empty list if the board does not have any + copper layers. """ copper_layers = [ ((side, use), layer) for (side, use), layer in self.graphic_layers.items() if use == 'copper' ] def sort_layername(val): @@ -905,17 +916,27 @@ class LayerStack: @property def top_side(self): + """ Return a dict containing the subset of layers from :py:meth:`~.layers.LayerStack.graphic_layers` that are on + the board's top side. Includes the board outline layer, if available. """ return { key: self[key] for key in ('top copper', 'top mask', 'top silk', 'top paste', 'mechanical outline') } @property def bottom_side(self): + """ Return a dict containing the subset of layers from :py:meth:`~.layers.LayerStack.graphic_layers` that are on + the board's bottom side. Includes the board outline layer, if available. """ return { key: self[key] for key in ('bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'mechanical outline') } @property def outline(self): + """ Return this board's outline layer if available, or :py:obj:`None`. """ return self.get('mechanical outline') def outline_svg_d(self, tol=0.01, unit=MM): + """ Return this board's outline as SVG path data. + + :param tol: :py:obj:`float` setting the tolerance below which two points are considered equal + :param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). SVG document unit. Default: mm + """ chains = self.outline_polygons(tol, unit) polys = [] for chain in chains: @@ -926,6 +947,19 @@ class LayerStack: return ' '.join(polys) def outline_polygons(self, tol=0.01, unit=MM): + """ Iterator yielding this boards outline as a list of ordered :py:class:`~.graphic_objects.Arc` and + :py:class:`~.graphic_objects.Line` objects. This method first sorts all lines and arcs on the outline layer into + connected components, then orders them such that one object's end point is the next object's start point, + flipping them where necessary. It yields one list of (likely mixed) :py:class:`~.graphic_objects.Arc` and + :py:class:`~.graphic_objects.Line` objects per connected component. + + This method exists because the only convention in Gerber or Excellon outline files is that the outline segments + are *visually contiguous*, but that does not necessarily mean that they will be in any particular order inside + the G-code. + + :param tol: :py:obj:`float` setting the tolerance below which two points are considered equal + :param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). SVG document unit. Default: mm + """ polygons = [] lines = [ obj.as_primitive(unit) for obj in self.outline.instance.objects if isinstance(obj, (go.Line, go.Arc)) ] @@ -975,7 +1009,7 @@ class LayerStack: yield l - def _merge_layer(self, target, source): + def _merge_layer(self, target, source, mode='above'): if source is None: return @@ -983,9 +1017,18 @@ class LayerStack: self[target] = source else: - self[target].merge(source) + self[target].merge(source, mode) - def merge(self, other): + def merge(self, other, mode='above'): + """ Merge ``other`` into ``self``, i.e. for all layers, add all objects that are in ``other`` to ``self``. This + resets :py:attr:`.import_settings` and :py:attr:`~.CamFile.generator` on all layers. Units and other + file-specific settings are handled automatically. For the meaning of the ``mode`` parameter, see + :py:meth:`.GerberFile.merge`. + + Layers are matched by their logical side and function as they are found in + :py:meth:`.LayerStack.graphic_layers`. Drill layers are normalized before merging, which splits them into + exactly three drill layers: An non-plated one, a plated one, and a (hopefully empty) unknown plating one. + """ all_keys = set(self.graphic_layers.keys()) | set(other.graphic_layers.keys()) exclude = { tuple(key.split()) for key in STANDARD_LAYERS } all_keys = { key for key in all_keys if key not in exclude } @@ -995,7 +1038,7 @@ class LayerStack: for side in 'top', 'bottom': for use in 'copper', 'mask', 'silk', 'paste': if (side, use) in other: - self._merge_layer((side, use), other[side, use]) + self._merge_layer((side, use), other[side, use], mode) our_inner, their_inner = self.copper_layers[1:-1], other.copper_layers[1:-1] diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py index 7bfd524..ce8b12a 100644 --- a/gerbonara/rs274x.py +++ b/gerbonara/rs274x.py @@ -46,6 +46,20 @@ def points_close(a, b): class GerberFile(CamFile): """ A single gerber file. + + :ivar objects: List of objects in this Gerber file. All elements must be subclasses of :py:class:`.GraphicObject`. + :ivar comments: List of string with textual comments in the source Gerber file. These are not saved by default, but + when you call :py:meth:`.GerberFile.save` with ``drop_comments=False``, the contents of this list + will be included as comments at the top of the output file. + :ivar generator_hints: List of strings indicating which EDA tool generated this file. Hints are added to this list + during file parsing whenever the parser encounters an idiosyncratic file format variation. + :ivar import_settings: File format settings used in the original file. This can be empty if this + :py:class:`.GerberFile` was generated programatically. + :ivar layer_hints: Similar to ``generator_hints``, this is a list containing hints which layer type this file could + belong to. Usually, this will be empty, but some EDA tools automatically include layer + information inside tool-specific comments in the Gerber files they generate. + :ivar apertures: List of apertures used in this file. Make sure you keep this in sync when adding new objects. + :ivar file_attrs: List of strings with Gerber X3 file attributes. Each list item corresponds to one file attribute. """ def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None, @@ -83,6 +97,16 @@ class GerberFile(CamFile): return def merge(self, other, mode='above', keep_settings=False): + """ Merge ``other`` into ``self``, i.e. add all objects that are in ``other`` to ``self``. This resets + :py:attr:`.import_settings` and :py:attr:`~.GerberFile.generator`. Units and other file-specific settings are + handled automatically. + + :param mode: One of the strings :py:obj:`"above"` (default) or :py:obj:`"below"`, specifying whether the other + layer's objects will be placed above this layer's objects (placing them towards the end of the file), or + below this layer's objects (placing them towards the beginning of the file). This setting is only relevant + when there are overlapping objects of different polarity, otherwise the rendered result will be the same + either way. + """ if other is None: return @@ -109,6 +133,7 @@ class GerberFile(CamFile): self.objects += other.objects else: raise ValueError(f'Invalid mode "{mode}", must be one of "above" or "below".') + for obj in self.objects: # If object has an aperture attribute, replace that aperture. if (ap := replace_apertures.get(id(getattr(obj, 'aperture', None)))): @@ -316,7 +341,8 @@ class GerberFile(CamFile): class GraphicsState: - """ Internal class used to track Gerber processing state during import and export. """ + """ Internal class used to track Gerber processing state during import and export. + """ def __init__(self, warn, file_settings=None, aperture_map=None): self.image_polarity = 'positive' # IP image polarity; deprecated @@ -527,7 +553,8 @@ class GraphicsState: class GerberParser: - """ Internal class that contains all of the actual Gerber parsing magic. """ + """ Internal class that contains all of the actual Gerber parsing magic. + """ NUMBER = r"[\+-]?\d+" DECIMAL = r"[\+-]?\d+([.]?\d+)?" -- cgit