From 460ea625af5c1d9e243feaa49923f7b2c7db8837 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 10 Jun 2022 00:39:07 +0200 Subject: Fix merging, bounding boxes and svg precision --- gerbonara/cam.py | 4 ++-- gerbonara/graphic_primitives.py | 6 ++++-- gerbonara/layers.py | 32 ++++++++++++++++++++++++++------ gerbonara/rs274x.py | 13 ++++++++++--- gerbonara/utils.py | 8 +++++--- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/gerbonara/cam.py b/gerbonara/cam.py index 3e00fb6..2cc57ba 100644 --- a/gerbonara/cam.py +++ b/gerbonara/cam.py @@ -236,7 +236,7 @@ class Polyline: (x0, y0), *rest = self.coords d = f'M {x0:.6} {y0:.6} ' + ' '.join(f'L {x:.6} {y:.6}' for x, y in rest) width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm' - return tag('path', d=d, style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linejoin: round; stroke-linecap: round') + return tag('path', d=d, style=f'fill: none; stroke: {color}; stroke-width: {width:.6}; stroke-linejoin: round; stroke-linecap: round') class CamFile: @@ -268,7 +268,7 @@ class CamFile: # setup viewport transform flipping y axis (content_min_x, content_min_y), (content_max_x, content_max_y) = bounds content_w, content_h = content_max_x - content_min_x, content_max_y - content_min_y - xform = f'translate({content_min_x} {content_min_y+content_h}) scale(1 -1) translate({-content_min_x} {-content_min_y})' + xform = f'translate({content_min_x:.6} {content_min_y+content_h:.6}) scale(1 -1) translate({-content_min_x:.6} {-content_min_y:.6})' tags = [tag('g', tags, transform=xform)] return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, diff --git a/gerbonara/graphic_primitives.py b/gerbonara/graphic_primitives.py index bfb1e89..7bbd3ff 100644 --- a/gerbonara/graphic_primitives.py +++ b/gerbonara/graphic_primitives.py @@ -23,6 +23,8 @@ from dataclasses import dataclass, KW_ONLY, replace from .utils import * +prec = lambda x: f'{x:.6}' + @dataclass class GraphicPrimitive: @@ -65,7 +67,7 @@ class Circle(GraphicPrimitive): def to_svg(self, fg='black', bg='white', tag=Tag): color = fg if self.polarity_dark else bg - return tag('circle', cx=self.x, cy=self.y, r=self.r, style=f'fill: {color}') + return tag('circle', cx=prec(self.x), cy=prec(self.y), r=prec(self.r), style=f'fill: {color}') @dataclass @@ -255,6 +257,6 @@ class Rectangle(GraphicPrimitive): def to_svg(self, fg='black', bg='white', tag=Tag): color = fg if self.polarity_dark else bg x, y = self.x - self.w/2, self.y - self.h/2 - return tag('rect', x=x, y=y, width=self.w, height=self.h, + return tag('rect', x=prec(x), y=prec(y), width=prec(self.w), height=prec(self.h), transform=svg_rotation(self.rotation, self.x, self.y), style=f'fill: {color}') diff --git a/gerbonara/layers.py b/gerbonara/layers.py index 9f42be8..c429910 100644 --- a/gerbonara/layers.py +++ b/gerbonara/layers.py @@ -203,11 +203,13 @@ def layername_autoguesser(fn): class LayerStack: - def __init__(self, graphic_layers, drill_layers, netlist=None, board_name=None): + def __init__(self, graphic_layers, drill_layers, netlist=None, board_name=None, original_path=None, was_zipped=False): self.graphic_layers = graphic_layers self.drill_layers = drill_layers self.board_name = board_name self.netlist = netlist + self.original_path = original_path + self.was_zipped = was_zipped @classmethod def open(kls, path, board_name=None, lazy=False): @@ -230,6 +232,8 @@ class LayerStack: inst = kls.from_directory(tmp_indir, board_name=board_name, lazy=lazy) inst.tmpdir = tmpdir + inst.original_path = filename + inst.was_zipped = True return inst @classmethod @@ -240,10 +244,12 @@ class LayerStack: raise FileNotFoundError(f'{directory} is not a directory') files = [ path for path in directory.glob('**/*') if path.is_file() ] - return kls.from_files(files, board_name=board_name, lazy=lazy) + return kls.from_files(files, board_name=board_name, lazy=lazy, original_path=directory) + inst.original_path = directory + return inst @classmethod - def from_files(kls, files, board_name=None, lazy=False): + def from_files(kls, files, board_name=None, lazy=False, original_path=None, was_zipped=False): generator, filemap = best_match(files) if sum(len(files) for files in filemap.values()) < 6: @@ -366,7 +372,16 @@ class LayerStack: board_name = common_prefix([l.original_path.name for l in layers.values() if l is not None]) board_name = re.sub(r'^\W+', '', board_name) board_name = re.sub(r'\W+$', '', board_name) - return kls(layers, drill_layers, netlist, board_name=board_name) + return kls(layers, drill_layers, netlist, board_name=board_name, + original_path=original_path, was_zipped=was_zipped) + + def save_to_zipfile(self, path, naming_scheme={}): + with tempfile.TemporaryDirectory() as tempdir: + self.save_to_directory(path, naming_scheme=naming_scheme) + with ZipFile(path) as le_zip: + for f in Path(tempdir.name).glob('*'): + with le_zip.open(f, 'wb') as out: + out.write(f.read_bytes()) def save_to_directory(self, path, naming_scheme={}, overwrite_existing=True): outdir = Path(path) @@ -483,12 +498,17 @@ class LayerStack: return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, pagecolor="white", tag=tag) - - def bounding_box(self, unit=MM, default=None): return sum_bounds(( layer.bounding_box(unit, default=default) for layer in itertools.chain(self.graphic_layers.values(), self.drill_layers) ), default=default) + + def board_bounds(self, unit=MM, default=None): + if self.outline: + return self.outline.instance.bounding_box(unit=unit, default=default) + else: + return self.bounding_box(unit=unit, default=default) + def merge_drill_layers(self): target = ExcellonFile(comments='Drill files merged by gerbonara') diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py index 5bfcc3f..1d18ec3 100644 --- a/gerbonara/rs274x.py +++ b/gerbonara/rs274x.py @@ -77,11 +77,12 @@ class GerberFile(CamFile): def to_gerber(self): return - def merge(self, other): + def merge(self, other, mode='above', keep_settings=False): if other is None: return - self.import_settings = None + if not keep_settings: + self.import_settings = None self.comments += other.comments # dedup apertures @@ -96,7 +97,13 @@ class GerberFile(CamFile): replace_apertures[id(ap)] = new_apertures[gbr] self.apertures = list(new_apertures.values()) - self.objects += other.objects + # Join objects + if mode == 'below': + self.objects = other.objects + self.objects + elif mode == 'above': + self.objects += other.objects + else: + raise ValueError(f'Invalid mode "{mode}", must be one of "above" or "below".') for obj in self.objects: # If object has an aperture attribute, replace that aperture. if (ap := replace_apertures.get(id(getattr(obj, 'aperture', None)))): diff --git a/gerbonara/utils.py b/gerbonara/utils.py index 38fa57e..290a44a 100644 --- a/gerbonara/utils.py +++ b/gerbonara/utils.py @@ -269,10 +269,12 @@ def sum_bounds(bounds, *, default=None): :rtype: tuple """ - if not bounds: - return default + bounds = iter(bounds) - ((min_x, min_y), (max_x, max_y)), *bounds = bounds + for (min_x, min_y), (max_x, max_y) in bounds: + break + else: + return default for (min_x_2, min_y_2), (max_x_2, max_y_2) in bounds: min_x, min_y = min_none(min_x, min_x_2), min_none(min_y, min_y_2) -- cgit