cli: Add merge command

This commit is contained in:
jaseg 2023-02-21 00:44:09 +01:00
parent a374483998
commit 9a6bc691cb
4 changed files with 196 additions and 92 deletions

View file

@ -2,9 +2,11 @@
import math
import click
import dataclasses
import re
import warnings
import json
import itertools
from pathlib import Path
from .utils import MM, Inch
@ -14,8 +16,6 @@ from .layers import LayerStack, NamingScheme
from . import __version__
NAMING_SCHEMES = [n for n in dir(NamingScheme) if not n.startswith('_')]
def print_version(ctx, param, value):
if value and not ctx.resilient_parsing:
click.echo(f'Version {__version__}')
@ -51,6 +51,44 @@ def apply_transform(transform, unit, layer_or_stack):
exec(transform, {key: value for key, value in math.__dict__.items() if not key.startswith('_')}, locals())
class Coordinate(click.ParamType):
name = 'coordinate'
def __init__(self, dimension=2):
self.dimension = dimension
def convert(self, value, param, ctx):
try:
coords = map(float, value.split(','))
if len(coords) != self.dimension:
raise ValueError()
return coords
except ValueError:
self.fail(f'{value!r} is not a valid coordinate. A coordinate consists of exactly {self.dimension} comma-separate floating-point numbers.')
class Unit(click.Choice):
name = 'unit'
def __init__(self):
super().__init__(['metric', 'us-customary'])
def convert(self, value, param, ctx):
value = super().convert(value, param, ctx)
return MM if value == 'metric' else Inch
class NamingScheme(click.Choice):
name = 'naming_scheme'
def __init__(self):
super().__init__([n for n in dir(NamingScheme) if not n.startswith('_')])
def convert(self, value, param, ctx):
return getattr(NamingScheme, super().convert(value, param, ctx))
@click.group()
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
def cli():
@ -58,8 +96,8 @@ def cli():
@cli.command()
@click.option('--format-warnings/--no-warnings', ' /-s', default=False, help='''Enable or disable file format warnings
during parsing (default: off)''')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
@click.option('-m', '--input-map', type=click.Path(exists=True, path_type=Path), help='''Extend or override layer name
mapping with name map from JSON file. The JSON file must contain a single JSON dict with an arbitrary
@ -70,8 +108,8 @@ def cli():
rules and use only rules given by --input-map''')
@click.option('--force-zip', is_flag=True, help='''Force treating input path as a zip file (default: guess file type
from extension and contents)''')
@click.option('--top/--bottom', help='Which side of the board to render')
@click.option('--command-line-units', type=click.Choice(['metric', 'us-customary']), default='metric', help='Units for values given in --transform. Default: millimeter')
@click.option('--top/--bottom', default=True, help='Which side of the board to render')
@click.option('--command-line-units', type=Unit(), default=MM, help='Units for values given in other options. Default: millimeter')
@click.option('--margin', type=float, default=0.0, help='Add space around the board inside the viewport')
@click.option('--force-bounds', help='Force SVG bounding box to value given as "min_x,min_y,max_x,max_y"')
@click.option('--inkscape/--standard-svg', default=True, help='Export in Inkscape SVG format with layers and stuff.')
@ -88,14 +126,12 @@ def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules,
overrides = json.loads(input_map.read_bytes()) if input_map else None
with warnings.catch_warnings():
warnings.simplefilter('default' if format_warnings else 'ignore')
warnings.simplefilter(format_warnings)
if force_zip:
stack = LayerStack.open_zip(inpath, overrides=overrides, autoguess=use_builtin_name_rules)
else:
stack = LayerStack.open(inpath, overrides=overrides, autoguess=use_builtin_name_rules)
unit = MM if command_line_units == 'metric' else Inch
if force_bounds:
min_x, min_y, max_x, max_y = list(map(float, force_bounds.split(',')))
force_bounds = (min_x, min_y), (max_x, max_y)
@ -103,32 +139,35 @@ def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules,
if colorscheme:
colorscheme = json.loads(colorscheme.read_text())
outfile.write(str(stack.to_pretty_svg(side='top' if top else 'bottom', margin=margin, arg_unit=unit, svg_unit=MM,
force_bounds=force_bounds, inkscape=inkscape, colors=colorscheme)))
outfile.write(str(stack.to_pretty_svg(side='top' if top else 'bottom', margin=margin, arg_unit=command_line_units,
svg_unit=MM, force_bounds=force_bounds, inkscape=inkscape, colors=colorscheme)))
@cli.command()
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
@click.option('--format-warnings/--no-warnings', ' /-s', default=True, help='''Enable or disable file format warnings
during parsing (default: on)''')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('-t', '--transform', help='''Execute python transformation script on input. You have access to the functions
translate(x, y), scale(factor) and rotate(angle, center_x?, center_y?), the bounding box variables x_min,
y_min, x_max, y_max, width and height, and everything from python\'s built-in math module (e.g. pi, sqrt,
sin). As convenience methods, center() and origin() are provided to center the board resp. move its
bottom-left corner to the origin. Coordinates are given in --command-line-units, angles in degrees, and
scale as a scale factor (as opposed to a percentage). 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('--command-line-units', type=Unit(), default=MM, help='Units for values given in other options. 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('-u', '--units', type=Unit(), 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('--reuse-input-settings', 'output_format', flag_value='reuse', help='''Use the same export settings as the
input file instead of sensible defaults.''')
@click.option('--default-settings', 'output_format', default=True, flag_value='defaults', help='''Use sensible defaults
for the output file format settings (default).''')
@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-units', type=Unit(), 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,
def rewrite(transform, command_line_units, number_format, units, zero_suppression, keep_comments, output_format,
input_number_format, input_units, input_zero_suppression, infile, outfile, format_warnings):
""" 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
@ -143,33 +182,26 @@ def rewrite(transform, command_line_units, number_format, units, zero_suppressio
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
input_settings.unit = input_units
with warnings.catch_warnings():
warnings.simplefilter('default' if format_warnings else 'ignore')
warnings.simplefilter(format_warnings)
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.defaults()
output_format = FileSettings() if output_format == 'reuse' else FileSettings.defaults()
if number_format:
a, _, b = number_format.partition('.')
output_settings.number_format = (int(a), int(b))
output_format.number_format = (int(a), int(b))
if units:
output_settings.unit = MM if units == 'metric' else Inch
output_format.unit = units
if zero_suppression:
output_settings.zeros = None if zero_suppression == 'off' else zero_suppression
output_format.zeros = None if zero_suppression == 'off' else zero_suppression
f.save(outfile, output_settings, not keep_comments)
f.save(outfile, output_format, not keep_comments)
@cli.command()
@ -181,29 +213,24 @@ def rewrite(transform, command_line_units, number_format, units, zero_suppressio
automatic guesses, or a gerbonara layer name such as "top copper", "inner_2 copper" or "bottom silk".''')
@click.option('--use-builtin-name-rules/--no-builtin-name-rules', default=True, help='''Disable built-in layer name
rules and use only rules given by --input-map''')
@click.option('--format-warnings/--no-warnings', ' /-s', default=True, help='''Enable or disable file format warnings
during parsing (default: on)''')
@click.option('--units', type=click.Choice(['metric', 'us-customary']), default='metric', help='''Units for values given
in transform script. Default: millimeter''')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('--units', type=Unit(), default=MM, help='Units for values given in other options. 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 for exported Gerber files. Note: This does not affect Excellon output, which *always*
uses explicit decimal points to avoid mismatches between output format and metadata in job files untouched
by gerbonara.''')
@click.option('--reuse-input-settings/--default-settings,', default=False, help='''Use the same export settings as the
@click.option('--reuse-input-settings', 'output_format', flag_value='reuse', help='''Use the same export settings as the
input file instead of sensible defaults.''')
@click.option('--default-settings', 'output_format', default=True, flag_value='defaults', help='''Use sensible defaults
for the output file format settings (default).''')
@click.option('--force-zip', is_flag=True, help='''Force treating input path as a zip file (default: guess file type
from extension and contents)''')
@click.option('--output-naming-scheme', type=click.Choice(NAMING_SCHEMES), help=f'''Name output files according to the
selected naming scheme instead of keeping the old file names. Supported values are:
{", ".join(NAMING_SCHEMES)}''')
@click.option('--output-naming-scheme', type=NamingScheme(), help=f'''Name output files according to the selected naming
scheme instead of keeping the old file names.''')
@click.argument('transform')
@click.argument('inpath')
@click.argument('outpath')
def transform(transform, units, number_format, zero_suppression, reuse_input_settings, inpath, outpath,
format_warnings, input_map, use_builtin_name_rules):
def transform(transform, units, output_format, inpath, outpath,
format_warnings, input_map, use_builtin_name_rules, output_naming_scheme):
""" Transform all gerber files in a given directory or zip file using the given python transformation script.
In the python transformation script you have access to the functions translate(x, y), scale(factor) and
@ -216,39 +243,92 @@ def transform(transform, units, number_format, zero_suppression, reuse_input_set
overrides = json.loads(input_map.read_bytes()) if input_map else None
with warnings.catch_warnings():
warnings.simplefilter('default' if format_warnings else 'ignore')
warnings.simplefilter(format_warnings)
if force_zip:
stack = LayerStack.open_zip(path, overrides=overrides, autoguess=use_builtin_name_rules)
else:
stack = LayerStack.open(path, overrides=overrides, autoguess=use_builtin_name_rules)
units = MM if units == 'metric' else Inch
apply_transform(transform, units, stack)
output_settings = FileSettings() if reuse_input_settings else FileSettings.defaults()
output_format = FileSettings() if output_format == 'reuse' else FileSettings.defaults()
stack.save_to_directory(outpath, naming_scheme=output_naming_scheme or {},
gerber_settings=output_format,
excellon_settings=dataclasses.replace(output_format, zeros=None))
if number_format:
a, _, b = number_format.partition('.')
output_settings.number_format = (int(a), int(b))
if units:
output_settings.unit = MM if units == 'metric' else Inch
@cli.command()
@click.option('--command-line-units', type=click.Choice(['metric', 'us-customary']), default='metric', help='Units for values given in --transform. Default: millimeter')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('--offset', multiple=True, type=Coordinate(), help="""Offset for the n'th file as a "x,y" string in unit
given by --command-line-units (default: millimeter). Can be given multiple times, and the first option
affects the first input, the second option affects the second input, and so on.""")
@click.option('--rotation', multiple=True, type=int, help="""Rotation for the n'th file in degrees clockwise. Can be
given multiple times, and the first option affects the first input, the second option affects the second
input, and so on.""")
@click.option('-m', '--input-map', type=click.Path(exists=True, path_type=Path), multiple=True, help='''Extend or
override layer name mapping with name map from JSON file. This option can be given multiple times, in
which case the n'th option affects only the n'th input, like with --offset and --rotation. The JSON file
must contain a single JSON dict with an arbitrary number of string: string entries. The keys are
interpreted as regexes applied to the filenames via re.fullmatch, and each value must either be the string
"ignore" to remove this layer from previous automatic guesses, or a gerbonara layer name such as "top
copper", "inner_2 copper" or "bottom silk".''')
@click.option('--reuse-input-settings', 'output_format', flag_value='reuse', help='''Use the same export settings as the
input file instead of sensible defaults.''')
@click.option('--default-settings', 'output_format', default=True, flag_value='defaults', help='''Use sensible defaults
for the output file format settings (default).''')
@click.option('--output-naming-scheme', type=NamingScheme(), help=f'''Name output files according to the selected naming
scheme instead of keeping the old file names of the first input.''')
@click.option('--output-board-name', help=f'''Override board name used with --output-naming-scheme''')
@click.option('--use-builtin-name-rules/--no-builtin-name-rules', default=True, help='''Disable built-in layer name
rules and use only rules given by --input-map''')
@click.argument('inpath', nargs=-1, type=click.Path(exists=True, path_type=Path))
@click.argument('outpath', type=click.Path(path_type=Path))
def merge(inpath, outpath, offset, rotation, input_map, command_line_units, output_format, output_naming_scheme,
output_board_name, format_warnings, use_builtin_name_rules):
""" Merge multiple single Gerber or Excellon files, or multiple stacks of Gerber files, into one. Hint: When used
with only one input, this command "normalizes" the input, converting all files to a well-defined, widely supported
Gerber subset with sane settings. When a --output-naming-scheme is given, it additionally renames all files to a
standardized naming convention. """
if not inpath:
return
if zero_suppression:
output_settings.zeros = None if zero_suppression == 'off' else zero_suppression
target = None
for p, offset, rotation, input_map in itertools.zip_longest(inpath, offset, rotation, input_map):
if p is None:
raise click.UsageError('More --offset, --rotation or --input-map options than input files')
stack.save_to_directory(outpath, naming_scheme=naming_scheme,
gerber_settings=output_settings,
excellon_settings=output_settings.replace(zeros=None))
offset = offset or (0, 0)
rotation = rotation or 0
overrides = json.loads(input_map.read_bytes()) if input_map else None
with warnings.catch_warnings():
warnings.simplefilter(format_warnings)
stack = LayerStack.open(p, overrides=overrides, autoguess=use_builtin_name_rules)
if target is None:
target = stack
else:
target.merge(stack)
if output_board_name:
if not output_naming_scheme:
warnings.warn('--output-board-name given without --output-naming-scheme. This will be ignored.')
target.board_name = output_board_name
output_format = FileSettings() if output_format == 'reuse' else FileSettings.defaults()
target.save_to_directory(outpath, naming_scheme=output_naming_scheme or {},
gerber_settings=output_format,
excellon_settings=dataclasses.replace(output_format, zeros=None))
@cli.command()
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
@click.option('--format-warnings/--no-warnings', ' /-s', default=True, help='''Enable or disable file format warnings
during parsing (default: on)''')
@click.option('--units', type=click.Choice(['us-customary', 'metric']), default='metric', help='Output bounding box in this unit (default: millimeter)')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('--units', type=Unit(), default=MM, 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-units', type=Unit(), 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, format_warnings, input_number_format, input_units, input_zero_suppression, units):
@ -265,26 +345,24 @@ def bounding_box(infile, format_warnings, input_number_format, input_units, inpu
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
input_settings.unit = input_units
with warnings.catch_warnings():
warnings.simplefilter('default' if format_warnings else 'ignore')
warnings.simplefilter(format_warnings)
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}]')
@cli.command()
@click.option('--format-warnings/--no-warnings', ' /-s', default=True, help='''Enable or disable file format warnings
during parsing (default: on)''')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('--force-zip', is_flag=True, help='Force treating input path as zip file (default: guess file type from extension and contents)')
@click.argument('path', type=click.Path(exists=True))
def layers(path, force_zip, format_warnings):
with warnings.catch_warnings():
warnings.simplefilter('default' if format_warnings else 'ignore')
warnings.simplefilter(format_warnings)
if force_zip:
stack = LayerStack.open_zip(path)
else:
@ -313,8 +391,8 @@ def layers(path, force_zip, format_warnings):
@cli.command()
@click.option('--format-warnings/--no-warnings', ' /-s', default=False, help='''Enable or disable file format warnings
during parsing (default: off)''')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), help='''Enable or
disable file format warnings during parsing (default: on)''')
@click.option('--force-zip', is_flag=True, help='Force treating input path as zip file (default: guess file type from extension and contents)')
@click.argument('path', type=click.Path(exists=True))
def meta(path, force_zip, format_warnings):
@ -322,7 +400,7 @@ def meta(path, force_zip, format_warnings):
the "layers" command. All lengths in the JSON are given in millimeter. """
with warnings.catch_warnings():
warnings.simplefilter('default' if format_warnings else 'ignore')
warnings.simplefilter(format_warnings)
if force_zip:
stack = LayerStack.open_zip(path)
else:
@ -348,20 +426,39 @@ def meta(path, force_zip, format_warnings):
for (side, function), layer in stack.graphic_layers.items():
d = out['graphical_layers'][side] = out['graphical_layers'].get(side, {})
(min_x, min_y), (max_x, max_y) = layer.bounding_box(default=((None, None), (None, None)))
if layer.import_settings:
numf = layer.import_settings.number_format
format_settings = {
'unit': str(layer.import_settings.unit),
'number_format': f'{numf[0]}.{numf[1]}' if numf else None,
'zero_suppression': str(layer.import_settings.zeros),
}
d[function] = {
'format': 'Gerber',
'path': str(layer.original_path),
'apertures': len(layer.apertures),
'objects': len(layer.objects),
'bounding_box': {'min_x': min_x, 'min_y': min_y, 'max_x': max_x, 'max_y': max_y},
'format_settings': format_settings,
}
out['drill_layers'] = []
for layer in stack.drill_layers:
if layer.import_settings:
numf = layer.import_settings.number_format
format_settings = {
'unit': str(layer.import_settings.unit),
'number_format': f'{numf[0]}.{numf[1]}' if numf else None,
'zero_suppression': str(layer.import_settings.zeros),
}
out['drill_layers'].append({
'format': 'Excellon',
'path': str(layer.original_path),
'plating': layer.plating_type,
'format_settings': format_settings,
})
print(json.dumps(out))

View file

@ -245,7 +245,7 @@ class ExcellonFile(CamFile):
def append(self, obj_or_comment):
""" Add a :py:class:`.GraphicObject` or a comment (str) to this file. """
if isinstnace(obj_or_comment, str):
if isinstance(obj_or_comment, str):
self.comments.append(obj_or_comment)
else:
self.objects.append(obj_or_comment)
@ -396,10 +396,10 @@ class ExcellonFile(CamFile):
if settings is None:
if self.import_settings:
settings = self.import_settings.copy()
settings.zeros = None
settings.number_format = (3,5)
else:
settings = FileSettings()
settings.zeros = None
settings.number_format = (3,5)
settings = FileSettings.defaults()
return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)).encode('utf-8')
def save(self, filename, settings=None, drop_comments=True):

View file

@ -471,7 +471,7 @@ class LayerStack:
if layer_type in naming_scheme:
path = naming_scheme[layer_type].format(layer_number=num, board_name=self.board_name)
elif layer.original_path.name:
elif layer.original_path:
path = layer.original_path.name
else:
path = f'{self.board_name}-{layer_type.replace(" ", "_")}.gbr'
@ -695,16 +695,16 @@ class LayerStack:
@property
def copper_layers(self):
copper_layers = [ (key, layer) for key, layer in self.layers.items() if key.endswith('copper') ]
copper_layers = [ ((side, use), layer) for (side, use), layer in self.graphic_layers.items() if use == 'copper' ]
def sort_layername(val):
key, _layer = val
if key.startswith('top'):
(side, use), _layer = val
if side == 'top':
return -1
if key.startswith('bottom'):
if side == 'bottom':
return 1e99
assert key.startswith('inner_')
return int(key[len('inner_'):])
assert side.startswith('inner_')
return int(side[len('inner_'):])
return [ layer for _key, layer in sorted(copper_layers, key=sort_layername) ]
@ -791,15 +791,16 @@ class LayerStack:
self[target].merge(source)
def merge(self, other):
all_keys = set(self.layers.keys()) | set(other.layers.keys())
exclude = { key.split() for key in STANDARD_LAYERS }
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 }
if all_keys:
warnings.warn('Cannot merge unknown layer types: {" ".join(all_keys)}')
for side in 'top', 'bottom':
for use in 'copper', 'mask', 'silk', 'paste':
self._merge_layer((side, use), other[side, use])
if (side, use) in other:
self._merge_layer((side, use), other[side, use])
our_inner, their_inner = self.copper_layers[1:-1], other.copper_layers[1:-1]
@ -834,5 +835,9 @@ class LayerStack:
self.drill_pth.merge(other.drill_pth)
self.drill_npth.merge(other.drill_npth)
self.drill_unknown.merge(other.drill_unknown)
self.netlist.merge(other.netlist)
if self.netlist:
self.netlist.merge(other.netlist)
else:
self.netlist = other.netlist

View file

@ -263,9 +263,11 @@ class GerberFile(CamFile):
:rtype: str
"""
if settings is None:
settings = self.import_settings.copy() or FileSettings()
settings.zeros = None
settings.number_format = (4,5) # up to 10m by 10m with 10nm resolution
if self.import_settings:
settings = self.import_settings.copy()
settings.zeros = None
else:
settings = FileSettings.defaults()
return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)).encode('utf-8')
def __len__(self):