More doc
This commit is contained in:
parent
ac66fd9d6b
commit
eaf4f21ce6
24 changed files with 543 additions and 108 deletions
|
|
@ -10,9 +10,15 @@ either a :py:class:`.GerberFile` or an :py:class:`.ExcellonFile`) is represented
|
|||
: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:`.CamFile` is the common base class for all layer types.
|
||||
|
||||
|
||||
.. autoclass:: gerbonara.layers.LayerStack
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.cam.CamFile
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerbonara.rs274x.GerberFile
|
||||
:members:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
Utilities
|
||||
=========
|
||||
|
||||
Physical units
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Gerbonara tracks length units using the :py:class:`.LengthUnit` class. :py:class:`.LengthUnit` contains a number of
|
||||
conventient conversion functions. Everywhere where Gerbonara accepts units as a method argument, it automatically
|
||||
converts a string ``'mm'`` or ``'inch'`` to the corresponding :py:class:`.LengthUnit`.
|
||||
|
||||
.. autoclass:: gerbonara.utils.LengthUnit
|
||||
:members:
|
||||
|
||||
Format settings
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
When reading or writing Gerber or Excellon, Gerbonara stores information about file format options such as zero
|
||||
suppression or number of decimal places in a :py:class:`.FileSettings` instance. When you are writing a Gerber file,
|
||||
Gerbonara picks reasonable defaults, but allows you to specify your own :py:class:`.FileSettings` to override these
|
||||
defaults.
|
||||
|
||||
.. autoclass:: gerbonara.cam.FileSettings
|
||||
:members:
|
||||
|
|
|
|||
42
gerbonara/NOTES
Normal file
42
gerbonara/NOTES
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
To do
|
||||
=====
|
||||
|
||||
[X] Actually use newly added gerber samples in test suite
|
||||
[X] Make Gerber parser error out if no unit is set anywhere
|
||||
[ ] Add test case for board outline / bounds with arcs (e.g. circle made up of four arcs, each with center line along
|
||||
x/y axis)
|
||||
[ ] Add "find outline" method
|
||||
[X] Refactor layer identification logic, automatically detect Allegro NCPARAM files
|
||||
[X] Add idempotence test: When reading, then reserializing the output of an earlier invocation of gerbonara, the output
|
||||
should not change. That is, in f1 --gn-> f2 --gn-> f3, it should be f2 == f3 but not necessarily f1 == f2.
|
||||
[X] Handle upverter output correctly: Upverter puts drils in a file called "design_export.xln" that actually contains
|
||||
Gerber, not Excellon
|
||||
[X] Add standard comment/attribute support for Gerber and Excellon
|
||||
[X] Add file/lineno info to all warnings and syntax errors
|
||||
[X] Make sure we handle arcs with co-inciding start/end points correctly (G74: no arc, G75: full circle)
|
||||
[ ] Add allegro drill test files with different zero suppression settings
|
||||
[ ] Add pcb-rnd to layer matching
|
||||
[ ] Add librepcb to layer matching
|
||||
[ ] On altium exports with multiple mech layers, use lowest-numbered one as board outline and raise a warning.
|
||||
[ ] Assign layer rules based on allegro metadata instead of filenames for allegro files
|
||||
[ ] Add more IPC-356 test files from github
|
||||
[X] Add IPC netlist support to LayerStack
|
||||
[ ] It seems the excellon generator never generates M16 (drill up) commands, only M15 (drill down) commands during
|
||||
routing
|
||||
[ ] Add standalone excellon SVG export test
|
||||
[ ] In image difference tests, detect empty images.
|
||||
[ ] Merge subsequent paths in gerbv svgs for less bad rendering performance
|
||||
[ ] Add integrated zip handling to layerstack
|
||||
[ ] Add GraphicObject.as(unit) method
|
||||
[ ] Add methods to graphic_object Line, Arc, Flash to convert between gerber and excellon representations.
|
||||
[ ] Add to_primitives to all *File classes
|
||||
[ ] Add region cut-in API
|
||||
[ ] Add radius- instead of center-based method of creating Arcs
|
||||
[ ] Add warning when interpolating aperture that is not either a circle or a rectangle.
|
||||
[ ] Maybe have Line and Arc just use a width instead of an aperture after all, and roll plating of excellon tool into
|
||||
graphic object subclass.
|
||||
[ ] Figure out whether to drop rectangular holes or whether to support them in polygon apertures as well.
|
||||
[ ] Add "number of parameters" property to ApertureMacro
|
||||
[ ] Aperture macro outline: Warn if first and last point are not the same.
|
||||
[ ] Make sure incremental mode actually works for gerber import
|
||||
|
|
@ -1,25 +1,26 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Gerbonara
|
||||
=========
|
||||
|
||||
gerbonara provides utilities for working with Gerber (RS-274X) and Excellon
|
||||
files in python.
|
||||
gerbonara provides utilities for working with Gerber (RS-274X) and Excellon files in python.
|
||||
"""
|
||||
|
||||
from .rs274x import GerberFile
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class ApertureMacro:
|
|||
return dup
|
||||
|
||||
|
||||
cons, var = ConstantExpression, VariableExpression
|
||||
var = VariableExpression
|
||||
deg_per_rad = 180 / math.pi
|
||||
|
||||
class GenericMacros:
|
||||
|
|
@ -179,3 +179,4 @@ if __name__ == '__main__':
|
|||
|
||||
for primitive in parse_macro(sys.stdin.read(), 'mm'):
|
||||
print(primitive)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, replace, field, fields, InitVar, KW_ONLY
|
||||
|
|
|
|||
134
gerbonara/cam.py
134
gerbonara/cam.py
|
|
@ -1,19 +1,21 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
#
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -27,20 +29,24 @@ from . import graphic_objects as go
|
|||
|
||||
@dataclass
|
||||
class FileSettings:
|
||||
'''
|
||||
''' Format settings for Gerber/Excellon import/export.
|
||||
|
||||
.. note::
|
||||
Format and zero suppression are configurable. Note that the Excellon
|
||||
and Gerber formats use opposite terminology with respect to leading
|
||||
and trailing zeros. The Gerber format specifies which zeros are
|
||||
suppressed, while the Excellon format specifies which zeros are
|
||||
included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use
|
||||
`zeros='trailing'`
|
||||
Format and zero suppression are configurable. Note that the Excellon and Gerber formats use opposite terminology
|
||||
with respect to leading and trailing zeros. The Gerber format specifies which zeros are suppressed, while the
|
||||
Excellon format specifies which zeros are included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use ``zeros='trailing'``
|
||||
'''
|
||||
#: Coordinate notation. ``'absolute'`` or ``'incremental'``. Absolute mode is universally used today. Incremental
|
||||
#: (relative) mode is technically still supported, but exceedingly rare in the wild.
|
||||
notation : str = 'absolute'
|
||||
#: Export unit. :py:attr:`~.utilities.MM` or :py:attr:`~.utilities.Inch`
|
||||
unit : LengthUnit = MM
|
||||
#: Angle unit. Should be ``'degree'`` unless you really know what you're doing.
|
||||
angle_unit : str = 'degree'
|
||||
#: Zero suppression settings. See note at :py:class:`.FileSettings` for meaning.
|
||||
zeros : bool = None
|
||||
#: Number format. ``(integer, decimal)`` tuple of number of integer and decimal digits. At most ``(6,7)`` by spec.
|
||||
number_format : tuple = (2, 5)
|
||||
|
||||
# input validation
|
||||
|
|
@ -64,6 +70,7 @@ class FileSettings:
|
|||
super().__setattr__(name, value)
|
||||
|
||||
def to_radian(self, value):
|
||||
""" Convert a given numeric string or a given float from file units into radians. """
|
||||
value = float(value)
|
||||
return math.radians(value) if self.angle_unit == 'degree' else value
|
||||
|
||||
|
|
@ -113,14 +120,15 @@ class FileSettings:
|
|||
return f'<File settings: unit={self.unit}/{self.angle_unit} notation={self.notation} zeros={self.zeros} number_format={self.number_format}>'
|
||||
|
||||
@property
|
||||
def incremental(self):
|
||||
def is_incremental(self):
|
||||
return self.notation == 'incremental'
|
||||
|
||||
@property
|
||||
def absolute(self):
|
||||
def is_absolute(self):
|
||||
return not self.incremental # default to absolute
|
||||
|
||||
def parse_gerber_value(self, value):
|
||||
""" Parse a numeric string in gerber format using this file's settings. """
|
||||
if not value:
|
||||
return None
|
||||
|
||||
|
|
@ -155,7 +163,7 @@ class FileSettings:
|
|||
return out
|
||||
|
||||
def write_gerber_value(self, value, unit=None):
|
||||
""" Convert a floating point number to a Gerber/Excellon-formatted string. """
|
||||
""" Convert a floating point number to a Gerber-formatted string. """
|
||||
|
||||
if unit is not None:
|
||||
value = self.unit(value, unit)
|
||||
|
|
@ -186,6 +194,7 @@ class FileSettings:
|
|||
return sign + (num or '0')
|
||||
|
||||
def write_excellon_value(self, value, unit=None):
|
||||
""" Convert a floating point number to an Excellon-formatted string. """
|
||||
if unit is not None:
|
||||
value = self.unit(value, unit)
|
||||
|
||||
|
|
@ -241,6 +250,10 @@ class Polyline:
|
|||
|
||||
|
||||
class CamFile:
|
||||
""" Base class for all layer classes (:py:class:`.GerberFile`, :py:class:`.ExcellonFile`, and :py:class:`.Netlist`).
|
||||
|
||||
Provides some common functions such as :py:meth:`~.CamFile.to_svg`.
|
||||
"""
|
||||
def __init__(self, original_path=None, layer_name=None, import_settings=None):
|
||||
self.original_path = original_path
|
||||
self.layer_name = layer_name
|
||||
|
|
@ -319,13 +332,29 @@ class CamFile:
|
|||
root=True)
|
||||
|
||||
def size(self, unit=MM):
|
||||
""" Get the dimensions of the file's axis-aligned bounding box, i.e. the difference in x- and y-direction
|
||||
between the minimum x and y coordinates and the maximum x and y coordinates.
|
||||
|
||||
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Which unit to return results in. Default: mm
|
||||
:returns: ``(w, h)`` tuple of floats.
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
(x0, y0), (x1, y1) = self.bounding_box(unit, default=((0, 0), (0, 0)))
|
||||
return (x1 - x0, y1 - y0)
|
||||
|
||||
def bounding_box(self, unit=MM, default=None):
|
||||
""" Calculate bounding box of file. Returns value given by 'default' argument when there are no graphical
|
||||
objects (default: None)
|
||||
""" Calculate the axis-aligned bounding box of file. Returns value given by the ``default`` argument when the
|
||||
file is empty. This file calculates the accurate bounding box, even for features such as arcs.
|
||||
|
||||
.. note:: Gerbonara returns bounding boxes as a ``(bottom_left, top_right)`` tuple of points, not in the
|
||||
``((min_x, max_x), (min_y, max_y))`` format used by pcb-tools.
|
||||
|
||||
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Which unit to return results in. Default: mm
|
||||
:returns: ``((x_min, y_min), (x_max, y_max))`` tuple of floats.
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
bounds = [ p.bounding_box(unit) for p in self.objects ]
|
||||
if not bounds:
|
||||
return default
|
||||
|
|
@ -335,10 +364,71 @@ class CamFile:
|
|||
max_x = max(x1 for (x0, y0), (x1, y1) in bounds)
|
||||
max_y = max(y1 for (x0, y0), (x1, y1) in bounds)
|
||||
|
||||
#for p in self.objects:
|
||||
# bb = (o_min_x, o_min_y), (o_max_x, o_max_y) = p.bounding_box(unit)
|
||||
# if o_min_x == min_x or o_min_y == min_y or o_max_x == max_x or o_max_y == max_y:
|
||||
# print('\033[91m bounds\033[0m', bb, p)
|
||||
|
||||
return ((min_x, min_y), (max_x, max_y))
|
||||
|
||||
def to_excellon(self):
|
||||
""" Convert to a :py:class:`.ExcellonFile`. Returns ``self`` if it already is one. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def to_gerber(self):
|
||||
""" Convert to a :py:class:`.GerberFile`. Returns ``self`` if it already is one. """
|
||||
raise NotImplementedError()
|
||||
|
||||
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.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def generator(self):
|
||||
""" Return our best guess as to which software produced this file.
|
||||
|
||||
:returns: a str like ``'kicad'`` or ``'allegro'``
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def offset(self, x=0, y=0, unit=MM):
|
||||
""" Add a coordinate offset to this file. The offset is given in Gerber/Excellon coordinates, so the Y axis
|
||||
points upwards. Gerbonara does not use the poorly-supported Gerber file offset options, but instead actually
|
||||
changes the coordinates of every object in the file. This means that you can load the generated file with any
|
||||
Gerber viewer, and things should just work.
|
||||
|
||||
:param float x: X offset
|
||||
:param float y: Y offset
|
||||
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Unit ``x`` and ``y`` are passed in. Default: mm
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def rotate(self, angle, cx=0, cy=0, unit=MM):
|
||||
""" Apply a rotation to this file. The center of rotation is given in Gerber/Excellon coordinates, so the Y axis
|
||||
points upwards. Gerbonara does not use the poorly-supported Gerber file rotation options, but instead actually
|
||||
changes the coordinates and rotation of every object in the file. This means that you can load the generated
|
||||
file with any Gerber viewer, and things should just work.
|
||||
|
||||
Note that when rotating certain apertures, they will be automatically converted to aperture macros during export
|
||||
since the standard apertures do not support rotation by spec. This is the same way most CAD packages deal with
|
||||
this issue so it should work with most Gerber viewers.
|
||||
|
||||
:param float angle: Rotation angle in radians, *clockwise*.
|
||||
:param float cx: Center of rotation X coordinate
|
||||
:param float cy: Center of rotation Y coordinate
|
||||
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Unit ``cx`` and ``cy`` are passed in. Default: mm
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
""" Check if there are any objects in this file. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def __len__(self):
|
||||
""" Return the number of objects in this file. Note that a e.g. a long trace or a long slot consisting of
|
||||
multiple segments is counted as one object per segment. Gerber regions are counted as only one object. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def __bool__(self):
|
||||
""" Test if this file contains any objects """
|
||||
raise NotImplementedError()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import math
|
||||
import operator
|
||||
|
|
@ -32,6 +34,8 @@ from .apertures import ExcellonTool
|
|||
from .utils import Inch, MM, to_unit, InterpMode, RegexMatcher
|
||||
|
||||
class ExcellonContext:
|
||||
""" Internal helper class used for tracking graphics state when writing Excellon. """
|
||||
|
||||
def __init__(self, settings, tools):
|
||||
self.settings = settings
|
||||
self.tools = tools
|
||||
|
|
@ -41,6 +45,7 @@ class ExcellonContext:
|
|||
self.drill_down = False
|
||||
|
||||
def select_tool(self, tool):
|
||||
""" Select the current tool. Retract drill first if necessary. """
|
||||
if self.current_tool != tool:
|
||||
if self.drill_down:
|
||||
yield 'M16' # drill up
|
||||
|
|
@ -50,6 +55,7 @@ class ExcellonContext:
|
|||
yield f'T{self.tools[id(tool)]:02d}'
|
||||
|
||||
def drill_mode(self):
|
||||
""" Enter drill mode. """
|
||||
if self.mode != ProgramState.DRILLING:
|
||||
self.mode = ProgramState.DRILLING
|
||||
if self.drill_down:
|
||||
|
|
@ -58,6 +64,7 @@ class ExcellonContext:
|
|||
yield 'G05' # drill mode
|
||||
|
||||
def route_mode(self, unit, x, y):
|
||||
""" Enter route mode and plunge tool at the given coordinates. """
|
||||
x, y = self.settings.unit(x, unit), self.settings.unit(y, unit)
|
||||
|
||||
if self.mode == ProgramState.ROUTING and (self.x, self.y) == (x, y):
|
||||
|
|
@ -74,9 +81,12 @@ class ExcellonContext:
|
|||
self.x, self.y = x, y
|
||||
|
||||
def set_current_point(self, unit, x, y):
|
||||
""" Update internal last point """
|
||||
self.x, self.y = self.settings.unit(x, unit), self.settings.unit(y, unit)
|
||||
|
||||
def parse_allegro_ncparam(data, settings=None):
|
||||
""" Internal function to parse Excellon format information out of Allegro's nonstandard textual parameter files that
|
||||
it generates along with the Excellon file. """
|
||||
# This function parses data from allegro's nc_param.txt and ncdrill.log files. We have to parse these files because
|
||||
# allegro Excellon files omit crucial information such as the *number format*. nc_param.txt really is the file we
|
||||
# want to parse, but sometimes due to user error it doesn't end up in the gerber package. In this case, we want to
|
||||
|
|
@ -125,6 +135,8 @@ def parse_allegro_ncparam(data, settings=None):
|
|||
|
||||
|
||||
def parse_allegro_logfile(data):
|
||||
""" Internal function to parse Excellon format information out of Allegro's nonstandard textual log files that it
|
||||
generates along with the Excellon file. """
|
||||
found_tools = {}
|
||||
unit = None
|
||||
|
||||
|
|
@ -150,6 +162,18 @@ def parse_allegro_logfile(data):
|
|||
return found_tools
|
||||
|
||||
class ExcellonFile(CamFile):
|
||||
""" Excellon drill file.
|
||||
|
||||
An Excellon file can contain both drills and milled slots. Drills are represented by :py:class:`.Flash` instances
|
||||
with their aperture set to the special :py:class:`.ExcellonDrill` aperture class. Drills can be plated or nonplated.
|
||||
This information is stored in the :py:class:`.ExcellonTool`. Both can co-exist in the same file, and some CAD tools
|
||||
even export files like this. :py:class:`.LayerStack` contains functions to convert between a single drill file with
|
||||
mixed plated and nonplated holes and one with separate drill files for each. Best practice is to have separate drill
|
||||
files for slots, nonplated holes, and plated holes, because the board house will produce all three in three separate
|
||||
processes anyway, and also because there is no standardized way to represent plating in Excellon files. Gerbonara
|
||||
uses Altium's convention for this, which uses a magic comment before the tool definition.
|
||||
"""
|
||||
|
||||
def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None):
|
||||
super().__init__(original_path=original_path)
|
||||
self.objects = objects or []
|
||||
|
|
@ -177,21 +201,26 @@ class ExcellonFile(CamFile):
|
|||
|
||||
@property
|
||||
def is_plated(self):
|
||||
""" Test if *all* holes or slots in this file are plated. """
|
||||
return all(obj.plated for obj in self.objects)
|
||||
|
||||
@property
|
||||
def is_nonplated(self):
|
||||
""" Test if *all* holes or slots in this file are non-plated. """
|
||||
return all(obj.plated == False for obj in self.objects) # False, not None
|
||||
|
||||
@property
|
||||
def is_plating_unknown(self):
|
||||
""" Test if *all* holes or slots in this file have no known plating. """
|
||||
return all(obj.plated is None for obj in self.objects) # False, not None
|
||||
|
||||
@property
|
||||
def is_mixed_plating(self):
|
||||
""" Test if there are multiple plating values used in this file. """
|
||||
return len({obj.plated for obj in self.objects}) > 1
|
||||
|
||||
def append(self, obj_or_comment):
|
||||
""" Add a :py:class:`.GraphicObject` or a comment (str) to this file. """
|
||||
if isinstnace(obj_or_comment, str):
|
||||
self.comments.append(obj_or_comment)
|
||||
else:
|
||||
|
|
@ -228,6 +257,23 @@ class ExcellonFile(CamFile):
|
|||
|
||||
@classmethod
|
||||
def open(kls, filename, plated=None, settings=None):
|
||||
""" Load an Excellon file from the file system.
|
||||
|
||||
Certain CAD tools do not put any information on decimal points into the actual excellon file, and instead put
|
||||
that information into a non-standard text file next to the excellon file. Using :py:meth:`~.ExcellonFile.open`
|
||||
to open a file gives Gerbonara the opportunity to try to find this data. In contrast to pcb-tools, Gerbonara
|
||||
will raise an exception instead of producing garbage parsing results if it cannot determine the file format
|
||||
parameters with certainty.
|
||||
|
||||
.. note:: This is preferred over loading Excellon from a str through :py:meth:`~.ExcellonFile.from_string`.
|
||||
|
||||
:param filename: ``str`` or ``pathlib.Path``.
|
||||
:param bool plated: If given, set plating status of any tools in this file that have undefined plating. This is
|
||||
useful if you already know that this file contains only e.g. plated holes from contextual information
|
||||
such as the file name.
|
||||
:param FileSettings settings: Format settings to use. If None, try to auto-detect file settings.
|
||||
"""
|
||||
|
||||
filename = Path(filename)
|
||||
logfile_tools = None
|
||||
|
||||
|
|
@ -250,13 +296,19 @@ class ExcellonFile(CamFile):
|
|||
|
||||
@classmethod
|
||||
def from_string(kls, data, settings=None, filename=None, plated=None, logfile_tools=None):
|
||||
""" Parse the given string as an Excellon file. Note that often, Excellon files do not contain any information
|
||||
on which number format (integer/decimal places, zeros suppression) is used. In case Gerbonara cannot determine
|
||||
this with certainty, this function *will* error out. Use :py:meth:`~.ExcellonFile.open` if you want Gerbonara to
|
||||
parse this metadata from the non-standardized text files many CAD packages produce in addition to drill files.
|
||||
"""
|
||||
|
||||
parser = ExcellonParser(settings, logfile_tools=logfile_tools)
|
||||
parser.do_parse(data, filename=filename)
|
||||
return kls(objects=parser.objects, comments=parser.comments, import_settings=settings,
|
||||
generator_hints=parser.generator_hints, original_path=filename)
|
||||
|
||||
def _generate_statements(self, settings, drop_comments=True):
|
||||
|
||||
""" Export this file as Excellon code, yields one str per line. """
|
||||
yield '; XNC file generated by gerbonara'
|
||||
if self.comments and not drop_comments:
|
||||
yield '; Comments found in original file:'
|
||||
|
|
@ -296,8 +348,17 @@ class ExcellonFile(CamFile):
|
|||
yield 'M30'
|
||||
|
||||
def generate_excellon(self, settings=None, drop_comments=True):
|
||||
''' Export to Excellon format. This function always generates XNC, which is a well-defined subset of Excellon.
|
||||
'''
|
||||
""" Export to Excellon format. This function always generates XNC, which is a well-defined subset of Excellon.
|
||||
Uses sane default settings if you don't give any.
|
||||
|
||||
|
||||
:param bool drop_comments: If true, do not write comments to output file. This defaults to true because
|
||||
otherwise there is a risk that Gerbonara does not consider some obscure magic comment semantically
|
||||
meaningful while some other Excellon viewer might still parse it.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if settings is None:
|
||||
if self.import_settings:
|
||||
settings = self.import_settings.copy()
|
||||
|
|
@ -308,6 +369,8 @@ class ExcellonFile(CamFile):
|
|||
return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments))
|
||||
|
||||
def save(self, filename, settings=None, drop_comments=True):
|
||||
""" Save this Excellon file to the file system. See :py:meth:`~.ExcellonFile.generate_excellon` for the meaning
|
||||
of the arguments. """
|
||||
with open(filename, 'w') as f:
|
||||
f.write(self.generate_excellon(settings, drop_comments=drop_comments))
|
||||
|
||||
|
|
@ -322,18 +385,6 @@ class ExcellonFile(CamFile):
|
|||
for obj in self.objects:
|
||||
obj.rotate(angle, cx, cy, unit=unit)
|
||||
|
||||
@property
|
||||
def has_mixed_plating(self):
|
||||
return len(set(obj.plated for obj in self.objects)) > 1
|
||||
|
||||
@property
|
||||
def is_plated(self):
|
||||
return all(obj.plated for obj in self.objects)
|
||||
|
||||
@property
|
||||
def is_nonplated(self):
|
||||
return not any(obj.plated for obj in self.objects)
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return not self.objects
|
||||
|
|
@ -342,6 +393,15 @@ class ExcellonFile(CamFile):
|
|||
return len(self.objects)
|
||||
|
||||
def split_by_plating(self):
|
||||
""" Split this file into two :py:class:`.ExcellonFile` instances, one containing all plated objects, and one
|
||||
containing all nonplated objects. In this function, objects with undefined plating are considered nonplated.
|
||||
|
||||
.. note:: This does not copy the objects, so modifications in either of the returned files may clobber the
|
||||
original file.
|
||||
|
||||
:returns: (nonplated_file, plated_file)
|
||||
:rtype: tuple
|
||||
"""
|
||||
plated = ExcellonFile(
|
||||
comments = self.comments.copy(),
|
||||
import_settings = self.import_settings.copy(),
|
||||
|
|
@ -356,15 +416,18 @@ class ExcellonFile(CamFile):
|
|||
|
||||
return nonplated, plated
|
||||
|
||||
def path_lengths(self, unit):
|
||||
def path_lengths(self, unit=MM):
|
||||
""" Calculate path lengths per tool.
|
||||
|
||||
Returns: dict { tool: float(path length) }
|
||||
|
||||
This function only sums actual cut lengths, and ignores travel lengths that the tool is doing without cutting to
|
||||
get from one object to another. Travel lengths depend on the CAM program's path planning, which highly depends
|
||||
on panelization and other factors. Additionally, an EDA tool will not even attempt to minimize travel distance
|
||||
as that's not its job.
|
||||
|
||||
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Unit to use for return value. Default: mm
|
||||
|
||||
:returns: ``{ tool: float(path length) }``
|
||||
:rtype dict:
|
||||
"""
|
||||
lengths = {}
|
||||
tool = None
|
||||
|
|
@ -377,31 +440,42 @@ class ExcellonFile(CamFile):
|
|||
return lengths
|
||||
|
||||
def hit_count(self):
|
||||
""" Calculate the number of objects per tool.
|
||||
|
||||
:rtype: collections.Counter
|
||||
"""
|
||||
return Counter(obj.tool for obj in self.objects)
|
||||
|
||||
def drill_sizes(self):
|
||||
return sorted({ obj.tool.diameter for obj in self.objects })
|
||||
def drill_sizes(self, unit=MM):
|
||||
""" Return a sorted list of all tool diameters found in this file.
|
||||
|
||||
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Unit to use for return values. Default: mm
|
||||
|
||||
:returns: list of floats, sorted smallest to largest diameter.
|
||||
:rtype: list
|
||||
"""
|
||||
# use equivalent_width for unit conversion
|
||||
return sorted({ obj.tool.equivalent_width(unit) for obj in self.objects })
|
||||
|
||||
def drills(self):
|
||||
""" Return all drilled hole objects in this file.
|
||||
|
||||
:returns: list of :py:class:`.Flash` instances
|
||||
:rtype: list
|
||||
"""
|
||||
return (obj for obj in self.objects if isinstance(obj, Flash))
|
||||
|
||||
def slots(self):
|
||||
""" Return all milled slot objects in this file.
|
||||
|
||||
:returns: list of :py:class:`~.graphic_objects.Line` or :py:class:`~.graphic_objects.Arc` instances
|
||||
:rtype: list
|
||||
"""
|
||||
return (obj for obj in self.objects if not isinstance(obj, Flash))
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
if not self.objects:
|
||||
return None
|
||||
|
||||
(x_min, y_min), (x_max, y_max) = self.objects[0].bounding_box()
|
||||
for obj in self.objects:
|
||||
(obj_x_min, obj_y_min), (obj_x_max, obj_y_max) = self.objects[0].bounding_box()
|
||||
x_min, y_min = min(x_min, obj_x_min), min(y_min, obj_y_min)
|
||||
x_max, y_max = max(x_max, obj_x_max), max(y_max, obj_y_max)
|
||||
|
||||
return ((x_min, y_min), (x_max, y_max))
|
||||
|
||||
class ProgramState(Enum):
|
||||
""" Internal helper class used to track Excellon program state (i.e. G05/G06 command state). """
|
||||
HEADER = 0
|
||||
DRILLING = 1
|
||||
ROUTING = 2
|
||||
|
|
@ -409,6 +483,8 @@ class ProgramState(Enum):
|
|||
|
||||
|
||||
class ExcellonParser(object):
|
||||
""" Internal helper class that contains all the actual Excellon format parsing logic. """
|
||||
|
||||
def __init__(self, settings=None, logfile_tools=None):
|
||||
# NOTE XNC files do not contain an explicit number format specification, but all values have decimal points.
|
||||
# Thus, we set the default number format to (None, None). If the file does not contain an explicit specification
|
||||
|
|
@ -634,7 +710,7 @@ class ExcellonParser(object):
|
|||
|
||||
old_pos = self.pos
|
||||
|
||||
if self.settings.absolute:
|
||||
if self.settings.is_absolute:
|
||||
if x is not None:
|
||||
self.pos = (x, self.pos[1])
|
||||
if y is not None:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import math
|
||||
import copy
|
||||
|
|
|
|||
|
|
@ -1,3 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import math
|
||||
import itertools
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
|
|
|
|||
|
|
@ -1,4 +1,22 @@
|
|||
# From https://github.com/tracespace/tracespace
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Based on https://github.com/tracespace/tracespace
|
||||
#
|
||||
|
||||
MATCH_RULES = {
|
||||
'altium': {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2021 Jan Götte <code@jaseg.de>
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
# Copyright 2021 Jan Götte <code@jaseg.de>
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -17,8 +17,7 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
""" This module provides an RS-274-X class and parser.
|
||||
"""
|
||||
#
|
||||
|
||||
import re
|
||||
import math
|
||||
|
|
@ -46,9 +45,7 @@ def points_close(a, b):
|
|||
return math.isclose(a[0], b[0]) and math.isclose(a[1], b[1])
|
||||
|
||||
class GerberFile(CamFile):
|
||||
""" A class representing a single gerber file
|
||||
|
||||
The GerberFile class represents a single gerber file.
|
||||
""" A single gerber file.
|
||||
"""
|
||||
|
||||
def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None,
|
||||
|
|
@ -81,7 +78,6 @@ class GerberFile(CamFile):
|
|||
return
|
||||
|
||||
def merge(self, other):
|
||||
""" Merge other GerberFile into this one """
|
||||
if other is None:
|
||||
return
|
||||
|
||||
|
|
@ -127,6 +123,7 @@ class GerberFile(CamFile):
|
|||
seen_macro_names.add(new_name)
|
||||
|
||||
def dilate(self, offset, unit=MM, polarity_dark=True):
|
||||
# TODO add tests for this
|
||||
self.apertures = [ aperture.dilated(offset, unit) for aperture in self.apertures ]
|
||||
|
||||
offset_circle = CircleAperture(offset, unit=unit)
|
||||
|
|
@ -154,6 +151,17 @@ class GerberFile(CamFile):
|
|||
|
||||
@classmethod
|
||||
def open(kls, filename, enable_includes=False, enable_include_dir=None):
|
||||
""" Load a Gerber file from the file system. The Gerber standard contains this wonderful and totally not
|
||||
insecure "include file" setting. We disable it by default and do not parse Gerber includes because a) nobody
|
||||
actually uses them, and b) they're a bad idea from a security point of view. In case you actually want these,
|
||||
you can enable them by setting ``enable_includes=True``.
|
||||
|
||||
:param filename: str or :py:class:`pathlib.Path`
|
||||
:param bool enable_includes: Enable Gerber ``IF`` statement includes (default *off*, recommended *off*)
|
||||
:param enable_include_dir: str or :py:class:`pathlib.Path`. Override base dir for include files.
|
||||
|
||||
:rtype: :py:class:`.GerberFile`
|
||||
"""
|
||||
filename = Path(filename)
|
||||
with open(filename, "r") as f:
|
||||
if enable_includes and enable_include_dir is None:
|
||||
|
|
@ -162,12 +170,15 @@ class GerberFile(CamFile):
|
|||
|
||||
@classmethod
|
||||
def from_string(kls, data, enable_include_dir=None, filename=None):
|
||||
""" Parse given string as Gerber file content. For the meaning of the parameters, see
|
||||
:py:meth:`~.GerberFile.open`. """
|
||||
# filename arg is for error messages
|
||||
obj = kls()
|
||||
GerberParser(obj, include_dir=enable_include_dir).parse(data, filename=filename)
|
||||
return obj
|
||||
|
||||
def generate_statements(self, settings, drop_comments=True):
|
||||
def _generate_statements(self, settings, drop_comments=True):
|
||||
""" Export this file as Gerber code, yields one str per line. """
|
||||
yield 'G04 Gerber file generated by Gerbonara*'
|
||||
for name, value in self.file_attrs.items():
|
||||
attrdef = ','.join([name, *map(str, value)])
|
||||
|
|
@ -222,17 +233,27 @@ class GerberFile(CamFile):
|
|||
return f'<GerberFile {name}with {len(self.apertures)} apertures, {len(self.objects)} objects>'
|
||||
|
||||
def save(self, filename, settings=None, drop_comments=True):
|
||||
""" Save this Gerber file to the file system. See :py:meth:`~.GerberFile.generate_gerber` for the meaning
|
||||
of the arguments. """
|
||||
with open(filename, 'w', encoding='utf-8') as f: # Encoding is specified as UTF-8 by spec.
|
||||
f.write(self.generate_gerber(settings, drop_comments=drop_comments))
|
||||
|
||||
def generate_gerber(self, settings=None, drop_comments=True):
|
||||
# Use given settings, or use same settings as original file if not given, or use defaults if not imported from a
|
||||
# file
|
||||
""" Export to Gerber format. Uses either the file's original settings or sane default settings if you don't give
|
||||
any.
|
||||
|
||||
:param FileSettings settings: override export settings.
|
||||
:param bool drop_comments: If true, do not write comments to output file. This defaults to true because
|
||||
otherwise there is a risk that Gerbonara does not consider some obscure magic comment semantically
|
||||
meaningful while some other Excellon viewer might still parse it.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if settings is None:
|
||||
settings = self.import_settings.copy() or FileSettings()
|
||||
settings.zeros = None
|
||||
settings.number_format = (5,6)
|
||||
return '\n'.join(self.generate_statements(settings, drop_comments=drop_comments))
|
||||
return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments))
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
|
|
@ -250,15 +271,6 @@ class GerberFile(CamFile):
|
|||
obj.with_offset(dx, dy, unit)
|
||||
|
||||
def rotate(self, angle:'radian', center=(0,0), unit=MM):
|
||||
""" Rotate file contents around given point.
|
||||
|
||||
Arguments:
|
||||
angle -- Rotation angle in radian clockwise.
|
||||
center -- Center of rotation (default: document origin (0, 0))
|
||||
|
||||
Note that when rotating by odd angles other than 0, 90, 180 or 270 degree this method may replace standard
|
||||
rect and oblong apertures by macro apertures. Existing macro apertures are re-written.
|
||||
"""
|
||||
if math.isclose(angle % (2*math.pi), 0):
|
||||
return
|
||||
|
||||
|
|
@ -271,11 +283,14 @@ class GerberFile(CamFile):
|
|||
obj.rotate(angle, *center, unit)
|
||||
|
||||
def invert_polarity(self):
|
||||
""" Invert the polarity (color) of each object in this file. """
|
||||
for obj in self.objects:
|
||||
obj.polarity_dark = not p.polarity_dark
|
||||
|
||||
|
||||
class GraphicsState:
|
||||
""" 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
|
||||
self.polarity_dark = True
|
||||
|
|
@ -502,6 +517,8 @@ class GraphicsState:
|
|||
|
||||
|
||||
class GerberParser:
|
||||
""" Internal class that contains all of the actual Gerber parsing magic. """
|
||||
|
||||
NUMBER = r"[\+-]?\d+"
|
||||
DECIMAL = r"[\+-]?\d+([.]?\d+)?"
|
||||
NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Based on https://github.com/tracespace/tracespace
|
||||
#
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
G04 file was processed by a buggy GerberTools version.
|
||||
G04 file was processed by a buggy GerberTools version.*
|
||||
G04 file manually fixed for GerberTools #86 / #143*
|
||||
%MOIN*%
|
||||
%OFA0B0*%
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
G04 From https://github.com/tracespace/tracespace/issues/365
|
||||
G04 From https://github.com/tracespace/tracespace/issues/365*
|
||||
G04 Generated by Cuprum (2.1.4) at 2021-06-09T12:46:32+02:00*
|
||||
%FSLAX66Y66*%
|
||||
%MOMM*%
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author: Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
import pytest
|
||||
|
||||
from ..ipc356 import *
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2021 Jan Götte <code@jaseg.de>
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -274,6 +275,22 @@ REFERENCE_DIRS = {
|
|||
'NCDrill/ThruHolePlated.ncd': 'drill plated',
|
||||
},
|
||||
|
||||
'zuken': {
|
||||
'': 'mechanical outline',
|
||||
'Gerber/DrillDrawingThrough.gdo': None,
|
||||
'Gerber/EtchLayerBottom.gdo': 'bottom copper',
|
||||
'Gerber/EtchLayerTop.gdo': 'top copper',
|
||||
'Gerber/GerberPlot.gpf': None,
|
||||
'Gerber/PCB.dsn': None,
|
||||
'Gerber/SolderPasteBottom.gdo': 'bottom paste',
|
||||
'Gerber/SolderPasteTop.gdo': 'top paste',
|
||||
'Gerber/SoldermaskBottom.gdo': 'bottom mask',
|
||||
'Gerber/SoldermaskTop.gdo': 'top mask',
|
||||
'NCDrill/ContourPlated.ncd': 'mechanical outline',
|
||||
'NCDrill/ThruHoleNonPlated.ncd': 'drill nonplated',
|
||||
'NCDrill/ThruHolePlated.ncd': 'drill plated',
|
||||
},
|
||||
|
||||
'upverter': {
|
||||
'design_export.drl': 'drill unknown',
|
||||
'design_export.gbl': 'bottom copper',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,23 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author: Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Based on https://github.com/tracespace/tracespace
|
||||
#
|
||||
|
||||
import math
|
||||
|
||||
from PIL import Image
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Author: Jan Götte <code@jaseg.de>
|
||||
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -1,3 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import pytest
|
||||
import functools
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
gerber.utils
|
||||
============
|
||||
**Gerber and Excellon file handling utilities**
|
||||
|
||||
This module provides utility functions for working with Gerber and Excellon
|
||||
files.
|
||||
This module provides utility functions for working with Gerber and Excellon files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue