Add CLI
This commit is contained in:
parent
fb52e10408
commit
f64b03efc7
7 changed files with 314 additions and 23 deletions
|
|
@ -142,6 +142,12 @@ class ApertureMacro:
|
|||
primitive.rotation -= rad_to_deg(angle)
|
||||
return dup
|
||||
|
||||
def scaled(self, scale):
|
||||
dup = copy.deepcopy(self)
|
||||
for primitive in dup.primitives:
|
||||
primitive.scale(scale)
|
||||
return dup
|
||||
|
||||
|
||||
var = VariableExpression
|
||||
deg_per_rad = 180 / math.pi
|
||||
|
|
|
|||
|
|
@ -93,6 +93,12 @@ class Circle(Primitive):
|
|||
def dilate(self, offset, unit):
|
||||
self.diameter += UnitExpression(offset, unit)
|
||||
|
||||
def scale(self, scale):
|
||||
self.x *= UnitExpression(scale)
|
||||
self.y *= UnitExpression(scale)
|
||||
self.diameter *= UnitExpression(scale)
|
||||
|
||||
|
||||
class VectorLine(Primitive):
|
||||
code = 20
|
||||
exposure : Expression
|
||||
|
|
@ -120,6 +126,12 @@ class VectorLine(Primitive):
|
|||
def dilate(self, offset, unit):
|
||||
self.width += UnitExpression(2*offset, unit)
|
||||
|
||||
def scale(self, scale):
|
||||
self.start_x *= UnitExpression(scale)
|
||||
self.start_y *= UnitExpression(scale)
|
||||
self.end_x *= UnitExpression(scale)
|
||||
self.end_y *= UnitExpression(scale)
|
||||
|
||||
|
||||
class CenterLine(Primitive):
|
||||
code = 21
|
||||
|
|
@ -142,6 +154,12 @@ class CenterLine(Primitive):
|
|||
|
||||
def dilate(self, offset, unit):
|
||||
self.width += UnitExpression(2*offset, unit)
|
||||
|
||||
def scale(self, scale):
|
||||
self.width *= UnitExpression(scale)
|
||||
self.height *= UnitExpression(scale)
|
||||
self.x *= UnitExpression(scale)
|
||||
self.y *= UnitExpression(scale)
|
||||
|
||||
|
||||
class Polygon(Primitive):
|
||||
|
|
@ -165,6 +183,11 @@ class Polygon(Primitive):
|
|||
def dilate(self, offset, unit):
|
||||
self.diameter += UnitExpression(2*offset, unit)
|
||||
|
||||
def scale(self, scale):
|
||||
self.diameter *= UnitExpression(scale)
|
||||
self.x *= UnitExpression(scale)
|
||||
self.y *= UnitExpression(scale)
|
||||
|
||||
|
||||
class Thermal(Primitive):
|
||||
code = 7
|
||||
|
|
@ -197,6 +220,13 @@ class Thermal(Primitive):
|
|||
# producing macros that may evaluate to primitives with negative values.
|
||||
warnings.warn('Attempted dilation of macro aperture thermal primitive. This is not supported.')
|
||||
|
||||
def scale(self, scale):
|
||||
self.d_outer *= UnitExpression(scale)
|
||||
self.d_inner *= UnitExpression(scale)
|
||||
self.gap_w *= UnitExpression(scale)
|
||||
self.x *= UnitExpression(scale)
|
||||
self.y *= UnitExpression(scale)
|
||||
|
||||
|
||||
class Outline(Primitive):
|
||||
code = 4
|
||||
|
|
@ -244,6 +274,9 @@ class Outline(Primitive):
|
|||
# we would need a whole polygon offset/clipping library here
|
||||
warnings.warn('Attempted dilation of macro aperture outline primitive. This is not supported.')
|
||||
|
||||
def scale(self, scale):
|
||||
self.coords = [(x*UnitExpression(scale), y*UnitExpression(scale)) for x, y in self.coords]
|
||||
|
||||
|
||||
class Comment:
|
||||
code = 0
|
||||
|
|
@ -254,6 +287,9 @@ class Comment:
|
|||
def to_gerber(self, unit=None):
|
||||
return f'0 {self.comment}'
|
||||
|
||||
def scale(self, scale):
|
||||
pass
|
||||
|
||||
PRIMITIVE_CLASSES = {
|
||||
**{cls.code: cls for cls in [
|
||||
Comment,
|
||||
|
|
|
|||
|
|
@ -251,6 +251,12 @@ class CircleAperture(Aperture):
|
|||
else:
|
||||
return self.to_macro(self.rotation)
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self,
|
||||
diameter=self.diameter*scale,
|
||||
hold_dia=None if self.hole_dia is None else self.hole_dia*scale,
|
||||
hold_rect_h=None if self.hole_rect_h is None else self.hole_rect_h*scale)
|
||||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM))
|
||||
|
||||
|
|
@ -300,6 +306,13 @@ class RectangleAperture(Aperture):
|
|||
else: # odd angle
|
||||
return self.to_macro()
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self,
|
||||
w=self.w*scale,
|
||||
h=self.h*scale,
|
||||
hold_dia=None if self.hole_dia is None else self.hole_dia*scale,
|
||||
hold_rect_h=None if self.hole_rect_h is None else self.hole_rect_h*scale)
|
||||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.rect,
|
||||
[MM(self.w, self.unit),
|
||||
|
|
@ -358,6 +371,13 @@ class ObroundAperture(Aperture):
|
|||
else:
|
||||
return self.to_macro()
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self,
|
||||
w=self.w*scale,
|
||||
h=self.h*scale,
|
||||
hold_dia=None if self.hole_dia is None else self.hole_dia*scale,
|
||||
hold_rect_h=None if self.hole_rect_h is None else self.hole_rect_h*scale)
|
||||
|
||||
def to_macro(self):
|
||||
# generic macro only supports w > h so flip x/y if h > w
|
||||
inst = self if self.w > self.h else replace(self, w=self.h, h=self.w, **_rotate_hole_90(self), rotation=self.rotation-90)
|
||||
|
|
@ -411,6 +431,11 @@ class PolygonAperture(Aperture):
|
|||
def _rotated(self):
|
||||
return self
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self,
|
||||
diameter=self.diameter*scale,
|
||||
hold_dia=None if self.hole_dia is None else self.hole_dia*scale)
|
||||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.polygon, self._params(MM))
|
||||
|
||||
|
|
@ -462,6 +487,9 @@ class ApertureMacroInstance(Aperture):
|
|||
def to_macro(self):
|
||||
return replace(self, macro=self.macro.rotated(self.rotation), rotation=0)
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self, macro=self.macro.scaled(scale))
|
||||
|
||||
def __eq__(self, other):
|
||||
return hasattr(other, 'macro') and self.macro == other.macro and \
|
||||
hasattr(other, 'parameters') and self.parameters == other.parameters and \
|
||||
|
|
|
|||
|
|
@ -44,17 +44,18 @@ class FileSettings:
|
|||
#: (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
|
||||
unit : LengthUnit = None
|
||||
#: 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.
|
||||
#: Zero suppression settings. Must be one of ``None``, ``'leading'`` or ``'trailing'``. 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)
|
||||
number_format : tuple = (None, None)
|
||||
|
||||
# input validation
|
||||
def __setattr__(self, name, value):
|
||||
if name == 'unit' and value not in [MM, Inch]:
|
||||
if name == 'unit' and value not in [None, MM, Inch]:
|
||||
raise ValueError(f'Unit must be either Inch or MM, not {value}')
|
||||
elif name == 'notation' and value not in ['absolute', 'incremental']:
|
||||
raise ValueError(f'Notation must be either "absolute" or "incremental", not {value}')
|
||||
|
|
@ -141,8 +142,8 @@ class FileSettings:
|
|||
|
||||
if '.' in value or value == '00':
|
||||
return float(value)
|
||||
|
||||
integer_digits, decimal_digits = self.number_format
|
||||
|
||||
integer_digits, decimal_digits = self.number_format or (2, 5)
|
||||
|
||||
if self.zeros == 'leading':
|
||||
value = self._pad + value # pad with zeros to ensure we have enough decimals
|
||||
|
|
@ -158,7 +159,7 @@ class FileSettings:
|
|||
if unit is not None:
|
||||
value = self.unit(value, unit)
|
||||
|
||||
integer_digits, decimal_digits = self.number_format
|
||||
integer_digits, decimal_digits = self.number_format or (2, 5)
|
||||
if integer_digits is None:
|
||||
integer_digits = 3
|
||||
if decimal_digits is None:
|
||||
|
|
@ -188,7 +189,7 @@ class FileSettings:
|
|||
if unit is not None:
|
||||
value = self.unit(value, unit)
|
||||
|
||||
integer_digits, decimal_digits = self.number_format
|
||||
integer_digits, decimal_digits = self.number_format or (2, 5)
|
||||
if integer_digits is None:
|
||||
integer_digits = 2
|
||||
if decimal_digits is None:
|
||||
|
|
|
|||
153
gerbonara/cli.py
Normal file
153
gerbonara/cli.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import click
|
||||
import re
|
||||
|
||||
from .utils import MM, Inch
|
||||
from .cam import FileSettings
|
||||
from .rs274x import GerberFile
|
||||
from . import __version__
|
||||
|
||||
|
||||
def print_version(ctx, param, value):
|
||||
click.echo(f'Version {__version__}')
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
|
||||
@click.argument('infiles', nargs=-1, required=True)
|
||||
@click.argument('outfile', required=False)
|
||||
def render(infiles, outfile):
|
||||
""" Render one or more gerber files into an SVG file. Can process entire folders or zip files of gerber files, and
|
||||
can render individual files from zips using "[zip file]:[member]" syntax. To specify a layer mapping, use
|
||||
"[layer]=[file]" syntax, e.g. "top-silk=something.zip:foo/bar.gbr". Layers get merged in the same order that they
|
||||
appear on the command line, and for each logical layer only the last given file is rendered."""
|
||||
|
||||
|
||||
def apply_transform(transform, unit, layer):
|
||||
for name, args, garbage in re.finditer(r'\s*([a-z]+)\s*\([\s-.0-9]*\)\s*|.*'):
|
||||
if name not in ('translate', 'scale', 'rotate'):
|
||||
raise ValueError(f'Unsupported transform {name}. Supported transforms are "translate", "scale" and "rotate".')
|
||||
|
||||
args = [float(args) for arg in args.split()]
|
||||
if not args:
|
||||
raise ValueError('No transform arguments given')
|
||||
|
||||
if name == 'translate':
|
||||
if len(args) != 2:
|
||||
raise ValueError(f'transform "translate" requires exactly two coordinates (x, and y), not {len(args)}')
|
||||
|
||||
x, y = args
|
||||
layer.offset(x, y, unit)
|
||||
|
||||
elif name == 'scale':
|
||||
if len(args) > 1:
|
||||
# We don't support non-uniform scaling with scale_x != scale_y since that isn't possible with straight
|
||||
# Gerber polygon or circular apertures, or holes.
|
||||
raise ValueError(f'transform "scale" requires exactly one argument, not {len(args)}')
|
||||
|
||||
layer.scale(*args)
|
||||
|
||||
elif name == 'rotate':
|
||||
if len(args) not in (1, 3):
|
||||
raise ValueError(f'transform "rotate" requires either one or three coordinates (angle, origin x, and origin y), not {len(args)}')
|
||||
|
||||
angle = args[0]
|
||||
cx, cy = args[1:] or (0, 0)
|
||||
layer.rotate(angle, cx, cy, unit)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
|
||||
@click.option('-t', '--transform', help='Apply transform given in pseudo-SVG syntax. Supported are "translate", "scale" and "rotate". Example: "translate(-10 0) rotate(45 0 5)"')
|
||||
@click.option('--command-line-units', type=click.Choice(['metric', 'us-customary']), default='metric', help='Units for values given in --transform. Default: millimeter')
|
||||
@click.option('-n', '--number-format', help='Override number format to use during export in "[integer digits].[decimal digits]" notation, e.g. "2.6".')
|
||||
@click.option('-u', '--units', type=click.Choice(['metric', 'us-customary']), help='Override export file units')
|
||||
@click.option('-z', '--zero-suppression', type=click.Choice(['off', 'leading', 'trailing']), help='Override export zero suppression setting. Note: The meaning of this value is like in the Gerber spec for both Gerber and Excellon files!')
|
||||
@click.option('--keep-comments/--drop-comments', help='Keep gerber comments. Note: Comments will be prepended to the start of file, and will not occur in their old position.')
|
||||
@click.option('--reuse-input-settings/--default-settings,', default=False, help='Use the same export settings as the input file instead of sensible defaults.')
|
||||
@click.option('--input-number-format', help='Override number format of input file (mostly useful for Excellon files)')
|
||||
@click.option('--input-units', type=click.Choice(['us-customary', 'metric']), help='Override units of input file')
|
||||
@click.option('--input-zero-suppression', type=click.Choice(['off', 'leading', 'trailing']), help='Override zero suppression setting of input file')
|
||||
@click.argument('infile')
|
||||
@click.argument('outfile')
|
||||
def rewrite(transform, command_line_units, number_format, units, zero_suppression, keep_comments, reuse_input_settings,
|
||||
input_number_format, input_units, input_zero_suppression, infile, outfile):
|
||||
""" Parse a gerber file, apply transformations, and re-serialize it into a new gerber file. Without transformations,
|
||||
this command can be used to convert a gerber file to use different settings (e.g. units, precision), but can also be
|
||||
used to "normalize" gerber files in a weird format into a more standards-compatible one as gerbonara's gerber parser
|
||||
is significantly more robust for weird inputs than others. """
|
||||
|
||||
input_settings = FileSettings()
|
||||
if input_number_format:
|
||||
a, _, b = input_number_format.partition('.')
|
||||
input_settings.number_format = (int(a), int(b))
|
||||
|
||||
if input_zero_suppression:
|
||||
input_settings.zeros = None if input_zero_suppression == 'off' else input_zero_suppression
|
||||
|
||||
if input_units:
|
||||
input_settings.unit = MM if input_units == 'metric' else Inch
|
||||
|
||||
f = GerberFile.open(infile, override_settings=input_settings)
|
||||
|
||||
if transform:
|
||||
command_line_units = MM if command_line_units == 'metric' else Inch
|
||||
apply_transform(transform, command_line_units, f)
|
||||
|
||||
if reuse_input_settings:
|
||||
output_settings = FileSettings()
|
||||
else:
|
||||
output_settings = FileSettings(unit=MM, number_format=(4,5), zeros=None)
|
||||
|
||||
if number_format:
|
||||
output_settings = number_format
|
||||
|
||||
if units:
|
||||
output_settings.unit = MM if units == 'metric' else Inch
|
||||
|
||||
if zero_suppression:
|
||||
output_settings.zeros = None if zero_suppression == 'off' else zero_suppression
|
||||
|
||||
f.save(outfile, output_settings, not keep_comments)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
|
||||
@click.option('--units', type=click.Choice(['us-customary', 'metric']), default='metric', help='Output bounding box in this unit (default: millimeter)')
|
||||
@click.option('--input-number-format', help='Override number format of input file (mostly useful for Excellon files)')
|
||||
@click.option('--input-units', type=click.Choice(['us-customary', 'metric']), help='Override units of input file')
|
||||
@click.option('--input-zero-suppression', type=click.Choice(['off', 'leading', 'trailing']), help='Override zero suppression setting of input file')
|
||||
@click.argument('infile')
|
||||
def bounding_box(infile, input_number_format, input_units, input_zero_suppression, units):
|
||||
""" Print the bounding box of a gerber file in "[x_min] [y_min] [x_max] [y_max]" format. The bounding box contains
|
||||
all graphic objects in this file, so e.g. a 100 mm by 100 mm square drawn with a 1mm width circular aperture will
|
||||
result in an 101 mm by 101 mm bounding box.
|
||||
"""
|
||||
|
||||
input_settings = FileSettings()
|
||||
if input_number_format:
|
||||
a, _, b = input_number_format.partition('.')
|
||||
input_settings.number_format = (int(a), int(b))
|
||||
|
||||
if input_zero_suppression:
|
||||
input_settings.zeros = None if input_zero_suppression == 'off' else input_zero_suppression
|
||||
|
||||
if input_units:
|
||||
input_settings.unit = MM if input_units == 'metric' else Inch
|
||||
|
||||
f = GerberFile.open(infile, override_settings=input_settings)
|
||||
units = MM if units == 'metric' else Inch
|
||||
(x_min, y_min), (x_max, y_max) = f.bounding_box(unit=units)
|
||||
print(f'{x_min:.6f} {y_min:.6f} {x_max:.6f} {y_max:.6f} [{units}]')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
|
||||
|
|
@ -105,6 +105,21 @@ class GraphicObject:
|
|||
dx, dy = self.unit(dx, unit), self.unit(dy, unit)
|
||||
self._offset(dx, dy)
|
||||
|
||||
def scale(self, sx, sy, unit=MM):
|
||||
""" Scale this feature in both its dimensions and location.
|
||||
|
||||
.. note:: The scale values are scalars, and the unit argument is irrelevant, but is kept for API consistency.
|
||||
|
||||
.. note:: If this object references an aperture, this aperture is not modified. You will have to transform this
|
||||
aperture yourself.
|
||||
|
||||
:param float sx: X scale, 1 to keep the object as is, larger values to enlarge, smaller values to shrink.
|
||||
Negative values are permitted.
|
||||
:param float sy: Y scale as above.
|
||||
"""
|
||||
|
||||
self._scale(sx, sy)
|
||||
|
||||
def rotate(self, rotation, cx=0, cy=0, unit=MM):
|
||||
""" Rotate this object. The center of rotation can be given in either unit, and is automatically converted into
|
||||
this object's local unit.
|
||||
|
|
@ -112,6 +127,9 @@ class GraphicObject:
|
|||
.. note:: The center's Y coordinate as well as the angle's polarity are flipped compared to computer graphics
|
||||
convention since Gerber uses a bottom-to-top Y axis.
|
||||
|
||||
.. note:: If this object references an aperture, this aperture is not modified. You will have to transform this
|
||||
aperture yourself.
|
||||
|
||||
:param float rotation: rotation in radians clockwise.
|
||||
:param float cx: X coordinate of center of rotation in *unit* units.
|
||||
:param float cy: Y coordinate of center of rotation. (0,0) is at the bottom left of the image.
|
||||
|
|
@ -214,6 +232,10 @@ class Flash(GraphicObject):
|
|||
def _rotate(self, rotation, cx=0, cy=0):
|
||||
self.x, self.y = gp.rotate_point(self.x, self.y, rotation, cx, cy)
|
||||
|
||||
def _scale(self, sx, sy):
|
||||
self.x *= sx
|
||||
self.y *= sy
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
yield from self.aperture.flash(conv.x, conv.y, unit, self.polarity_dark)
|
||||
|
|
@ -281,6 +303,12 @@ class Region(GraphicObject):
|
|||
(arc[0], gp.rotate_point(*arc[1], angle, cx-p[0], cy-p[1])) if arc else None
|
||||
for p, arc in zip(self.outline, self.arc_centers) ]
|
||||
|
||||
def _scale(self, sx, sy):
|
||||
self.outline = [ (x*sx, y*sy) for x, y in self.outline ]
|
||||
self.arc_centers = [
|
||||
(arc[0], (arc[1][0]*sx, arc[1][1]*sy)) if arc else None
|
||||
for p, arc in zip(self.outline, self.arc_centers) ]
|
||||
|
||||
def append(self, obj):
|
||||
if obj.unit != self.unit:
|
||||
obj = obj.converted(self.unit)
|
||||
|
|
@ -379,6 +407,12 @@ class Line(GraphicObject):
|
|||
self.x1, self.y1 = gp.rotate_point(self.x1, self.y1, rotation, cx, cy)
|
||||
self.x2, self.y2 = gp.rotate_point(self.x2, self.y2, rotation, cx, cy)
|
||||
|
||||
def _scale(self, sx=1, sy=1):
|
||||
self.x1 *= sx
|
||||
self.y1 *= sy
|
||||
self.x2 *= sx
|
||||
self.y2 *= sy
|
||||
|
||||
@property
|
||||
def p1(self):
|
||||
""" Convenience alias for ``(self.x1, self.y1)`` returning start point of the line. """
|
||||
|
|
@ -611,6 +645,14 @@ class Arc(GraphicObject):
|
|||
self.x2, self.y2 = gp.rotate_point(self.x2, self.y2, rotation, cx, cy)
|
||||
self.cx, self.cy = new_cx - self.x1, new_cy - self.y1
|
||||
|
||||
def _scale(self, sx=1, sy=1):
|
||||
self.x1 *= sx
|
||||
self.y1 *= sy
|
||||
self.x2 *= sx
|
||||
self.y2 *= sy
|
||||
self.cx *= sx
|
||||
self.cy *= sy
|
||||
|
||||
def as_primitive(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ class GerberFile(CamFile):
|
|||
self.primitives.extend(new_primitives)
|
||||
|
||||
@classmethod
|
||||
def open(kls, filename, enable_includes=False, enable_include_dir=None):
|
||||
def open(kls, filename, enable_includes=False, enable_include_dir=None, override_settings=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,
|
||||
|
|
@ -173,15 +173,16 @@ class GerberFile(CamFile):
|
|||
with open(filename, "r") as f:
|
||||
if enable_includes and enable_include_dir is None:
|
||||
enable_include_dir = filename.parent
|
||||
return kls.from_string(f.read(), enable_include_dir, filename=filename)
|
||||
return kls.from_string(f.read(), enable_include_dir, filename=filename, override_settings=override_settings)
|
||||
|
||||
@classmethod
|
||||
def from_string(kls, data, enable_include_dir=None, filename=None):
|
||||
def from_string(kls, data, enable_include_dir=None, filename=None, override_settings=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)
|
||||
parser = GerberParser(obj, include_dir=enable_include_dir, override_settings=override_settings)
|
||||
parser.parse(data, filename=filename)
|
||||
return obj
|
||||
|
||||
def _generate_statements(self, settings, drop_comments=True):
|
||||
|
|
@ -194,8 +195,10 @@ class GerberFile(CamFile):
|
|||
|
||||
zeros = 'T' if settings.zeros == 'trailing' else 'L' # default to leading if "None" is specified
|
||||
notation = 'I' if settings.notation == 'incremental' else 'A' # default to absolute
|
||||
number_format = str(settings.number_format[0]) + str(settings.number_format[1])
|
||||
yield f'%FS{zeros}{notation}X{number_format}Y{number_format}*%'
|
||||
num_int, num_frac = settings.number_format or (4,5)
|
||||
assert 1 <= num_int <= 9
|
||||
assert 1 <= num_frac <= 9
|
||||
yield f'%FS{zeros}{notation}X{num_int}{num_frac}Y{num_int}{num_frac}*%'
|
||||
yield '%IPPOS*%'
|
||||
yield 'G75'
|
||||
yield '%LPD*%'
|
||||
|
|
@ -262,12 +265,23 @@ class GerberFile(CamFile):
|
|||
if settings is None:
|
||||
settings = self.import_settings.copy() or FileSettings()
|
||||
settings.zeros = None
|
||||
settings.number_format = (5,6)
|
||||
settings.number_format = (4,5) # up to 10m by 10m with 10nm resolution
|
||||
return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)).encode('utf-8')
|
||||
|
||||
def __len__(self):
|
||||
return len(self.objects)
|
||||
|
||||
def scale(self, scale, unit=MM):
|
||||
scaled_apertures = {}
|
||||
|
||||
for obj in self.objects:
|
||||
obj.scale(sx, sy)
|
||||
|
||||
if (aperture := getattr(obj, 'aperture', None)):
|
||||
if not (scaled := scaled_apertures.get(aperture)):
|
||||
scaled = scaled_apertures[aperture] = aperture.scaled(scale)
|
||||
obj.aperture = scaled
|
||||
|
||||
def offset(self, dx=0, dy=0, unit=MM):
|
||||
# TODO round offset to file resolution
|
||||
for obj in self.objects:
|
||||
|
|
@ -545,12 +559,12 @@ class GerberParser:
|
|||
'comment': r"G0?4(?P<comment>[^*]*)",
|
||||
}
|
||||
|
||||
def __init__(self, target, include_dir=None):
|
||||
def __init__(self, target, include_dir=None, override_settings=None):
|
||||
""" Pass an include dir to enable IF include statements (potentially DANGEROUS!). """
|
||||
self.target = target
|
||||
self.include_dir = include_dir
|
||||
self.include_stack = []
|
||||
self.file_settings = FileSettings()
|
||||
self.file_settings = override_settings or FileSettings()
|
||||
self.graphics_state = GraphicsState(warn=self.warn, file_settings=self.file_settings)
|
||||
self.aperture_map = {}
|
||||
self.aperture_macros = {}
|
||||
|
|
@ -774,19 +788,30 @@ class GerberParser:
|
|||
match['name'], match['macro'], self.file_settings.unit)
|
||||
|
||||
def _parse_format_spec(self, match):
|
||||
# This is a common problem in Eagle files, so just suppress it
|
||||
self.file_settings.zeros = {'L': 'leading', 'T': 'trailing'}.get(match['zero'], 'leading')
|
||||
if self.file_settings.zeros is not None:
|
||||
self.warn('Re-definition of zero suppression setting. Ignoring.')
|
||||
else:
|
||||
# This is a common problem in Eagle files, so just suppress it
|
||||
self.file_settings.zeros = {'L': 'leading', 'T': 'trailing'}.get(match['zero'], 'leading')
|
||||
|
||||
self.file_settings.notation = 'incremental' if match['notation'] == 'I' else 'absolute'
|
||||
|
||||
if match['x'] != match['y']:
|
||||
raise SyntaxError(f'FS specifies different coordinate formats for X and Y ({match["x"]} != {match["y"]})')
|
||||
self.file_settings.number_format = int(match['x'][0]), int(match['x'][1])
|
||||
|
||||
if self.file_settings.number_format != (None, None):
|
||||
self.warn('Re-definition of number format setting. Ignoring.')
|
||||
else:
|
||||
self.file_settings.number_format = int(match['x'][0]), int(match['x'][1])
|
||||
|
||||
def _parse_unit_mode(self, match):
|
||||
if match['unit'] == 'MM':
|
||||
self.graphics_state.unit = self.file_settings.unit = MM
|
||||
if self.file_settings.unit is not None:
|
||||
self.warn('Re-definition of file units. Ignoring.')
|
||||
else:
|
||||
self.graphics_state.unit = self.file_settings.unit = Inch
|
||||
if match['unit'] == 'MM':
|
||||
self.graphics_state.unit = self.file_settings.unit = MM
|
||||
else:
|
||||
self.graphics_state.unit = self.file_settings.unit = Inch
|
||||
|
||||
def _parse_allegro_format_spec(self, match):
|
||||
self._parse_format_spec(match)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue