From 07b2628dbb08de7120ef3c760cd91f0d8901fe73 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 4 Apr 2023 19:06:04 +0200 Subject: Various convenience improvements, and make board name guessing really smart --- gerbonara/cam.py | 6 +++--- gerbonara/graphic_primitives.py | 4 ++-- gerbonara/layers.py | 46 +++++++++++++++++++++++++++++++---------- gerbonara/rs274x.py | 5 +++++ gerbonara/utils.py | 18 ++++++++++++---- 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/gerbonara/cam.py b/gerbonara/cam.py index 14d4c61..49c2df3 100644 --- a/gerbonara/cam.py +++ b/gerbonara/cam.py @@ -247,9 +247,9 @@ class Polyline: return None (x0, y0), *rest = self.coords - d = f'M {x0:.6} {y0:.6} ' + ' '.join(f'L {x:.6} {y:.6}' for x, y in rest) + d = f'M {float(x0):.6} {float(y0):.6} ' + ' '.join(f'L {float(x):.6} {float(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:.6}; stroke-linejoin: round; stroke-linecap: round') + return tag('path', d=d, style=f'fill: none; stroke: {color}; stroke-width: {float(width):.6}; stroke-linejoin: round; stroke-linecap: round') class CamFile: @@ -283,7 +283,7 @@ class CamFile: content_min_x, content_min_y = float(content_min_x), float(content_min_y) content_max_x, content_max_y = float(content_max_x), float(content_max_y) content_w, content_h = content_max_x - content_min_x, content_max_y - 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})' + xform = f'translate({float(content_min_x):.6} {float(content_min_y+content_h):.6}) scale(1 -1) translate({-float(content_min_x):.6} {-float(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 1cfebbb..27890b1 100644 --- a/gerbonara/graphic_primitives.py +++ b/gerbonara/graphic_primitives.py @@ -188,7 +188,7 @@ class Line(GraphicPrimitive): def to_svg(self, fg='black', bg='white', tag=Tag): color = fg if self.polarity_dark else bg width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm' - return tag('path', d=f'M {self.x1:.6} {self.y1:.6} L {self.x2:.6} {self.y2:.6}', + return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} L {float(self.x2):.6} {float(self.y2):.6}', style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round') @dataclass(frozen=True) @@ -240,7 +240,7 @@ class Arc(GraphicPrimitive): color = fg if self.polarity_dark else bg arc = svg_arc((self.x1, self.y1), (self.x2, self.y2), (self.cx, self.cy), self.clockwise) width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm' - return tag('path', d=f'M {self.x1:.6} {self.y1:.6} {arc}', + return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} {arc}', style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round; fill: none') @dataclass(frozen=True) diff --git a/gerbonara/layers.py b/gerbonara/layers.py index 6200ef5..6c61756 100644 --- a/gerbonara/layers.py +++ b/gerbonara/layers.py @@ -296,8 +296,8 @@ class LayerStack: 'bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'mechanical outline')} - drill_pth = ExcellonFile(plated=True) - drill_npth = ExcellonFile(plated=False) + drill_pth = ExcellonFile() + drill_npth = ExcellonFile() self.graphic_layers = graphic_layers self.drill_pth = drill_pth @@ -557,7 +557,7 @@ class LayerStack: return kls(layers, drill_pth, drill_npth, drill_layers, board_name=board_name, original_path=original_path, was_zipped=was_zipped, generator=[*all_generator_hints, None][0]) - def save_to_zipfile(self, path, prefix='', overwrite_existing=True, naming_scheme={}, + def save_to_zipfile(self, path, prefix='', overwrite_existing=True, board_name=None, naming_scheme={}, gerber_settings=None, excellon_settings=None): """ Save this board into a zip file at the given path. For other options, see :py:meth:`~.layers.LayerStack.save_to_directory`. @@ -565,6 +565,7 @@ class LayerStack: :param path: Path of output zip file :param overwrite_existing: Bool specifying whether override an existing zip file. If :py:obj:`False` and :py:obj:`path` exists, a :py:obj:`ValueError` is raised. + :param board_name: Board name to use when naming the Gerber/Excellon files :param prefix: Store output files under the given prefix inside the zip file """ @@ -578,11 +579,11 @@ class LayerStack: excellon_settings = gerber_settings with ZipFile(path, 'w') as le_zip: - for path, layer in self._save_files_iter(naming_scheme=naming_scheme): + for path, layer in self._save_files_iter(board_name=board_name, naming_scheme=naming_scheme): with le_zip.open(prefix + str(path), 'w') as out: out.write(layer.instance.write_to_bytes()) - def save_to_directory(self, path, naming_scheme={}, overwrite_existing=True, + def save_to_directory(self, path, naming_scheme={}, overwrite_existing=True, board_name=None, gerber_settings=None, excellon_settings=None): """ Save this board into a directory at the given path. If the given path does not exist, a new directory is created in its place. @@ -593,6 +594,7 @@ class LayerStack: scheme is used. You can provide your own :py:obj:`dict` here, mapping :py:obj:`"side use"` strings to filenames, or use one of :py:attr:`~.layers.NamingScheme.kicad` or :py:attr:`~.layers.NamingScheme.kicad`. + :param board_name: Board name to use when naming the Gerber/Excellon files :param overwrite_existing: Bool specifying whether override an existing directory. If :py:obj:`False` and :py:obj:`path` exists, a :py:obj:`ValueError` is raised. Note that a :py:obj:`ValueError` will still be raised if the target exists and is not a @@ -608,15 +610,32 @@ class LayerStack: if gerber_settings and not excellon_settings: excellon_settings = gerber_settings - for path, layer in self._save_files_iter(naming_scheme=naming_scheme): + for path, layer in self._save_files_iter(board_name=board_name, naming_scheme=naming_scheme): out = outdir / path if out.exists() and not overwrite_existing: raise SystemError(f'Path exists but overwrite_existing is False: {out}') layer.instance.save(out) - def _save_files_iter(self, naming_scheme={}): + def _save_files_iter(self, board_name=None, naming_scheme={}): + board_name = board_name or self.board_name + + if board_name is None: + import inspect + frame = inspect.currentframe() + if frame is None: + board_name = 'board' + else: + while frame is not None: + import sys + if not frame.f_globals['__name__'].startswith('gerbonara'): + board_name = frame.f_code.co_name + del frame + break + old_frame, frame = frame, frame.f_back + del old_frame + def get_name(layer_type, layer): - nonlocal naming_scheme + nonlocal naming_scheme, board_name if (m := re.match('inner_([0-9]+) copper', layer_type)): layer_type = 'inner copper' @@ -625,11 +644,13 @@ class LayerStack: num = None if layer_type in naming_scheme: - path = naming_scheme[layer_type].format(layer_number=num, board_name=self.board_name) + path = naming_scheme[layer_type].format(layer_number=num, board_name=board_name) elif layer.original_path and layer.original_path.name: path = layer.original_path.name else: - path = f'{self.board_name}-{layer_type.replace(" ", "_")}.gbr' + path = NamingScheme.kicad[layer_type].format(layer_number=num, board_name=board_name) + #ext = 'drl' if isinstance(layer, ExcellonFile) else 'gbr' + #path = f'{board_name}-{layer_type.replace(" ", "_")}.{ext}' return path @@ -664,6 +685,9 @@ class LayerStack: unwanted. If you want to instead generate a nice-looking preview image for display or graphical editing in tools such as Inkscape, use :py:meth:`~.layers.LayerStack.to_pretty_svg` instead. + WARNING: The SVG files generated by this function preserve the Gerber coordinates 1:1, so the file will be + mirrored vertically. + :param margin: Export SVG file with given margin around the board's bounding box. :param arg_unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Which unit ``margin`` and ``force_bounds`` are specified in. Default: mm @@ -689,7 +713,7 @@ class LayerStack: tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg='black', bg="white", tag=Tag)), id=f'l-drill-{i}')) - return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, pagecolor=page_bg, tag=tag) + return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, tag=tag) def to_pretty_svg(self, side='top', margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, tag=Tag, inkscape=False, colors=None): diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py index 6620f10..271880b 100644 --- a/gerbonara/rs274x.py +++ b/gerbonara/rs274x.py @@ -73,6 +73,9 @@ class GerberFile(CamFile): self.apertures = [] # FIXME get rid of this? apertures are already in the objects. self.file_attrs = file_attrs or {} + def sync_apertures(self): + self.apertures = list({id(obj.aperture): obj.aperture for obj in self.objects if hasattr(obj, 'aperture')}.values()) + def to_excellon(self, plated=None, errors='raise'): """ Convert this excellon file into a :py:class:`~.excellon.ExcellonFile`. This will convert interpolated lines into slots, and circular aperture flashes into holes. Other features such as ``G36`` polygons or flashes with @@ -227,6 +230,8 @@ class GerberFile(CamFile): def _generate_statements(self, settings, drop_comments=True): """ Export this file as Gerber code, yields one str per line. """ + self.sync_apertures() + yield 'G04 Gerber file generated by Gerbonara*' for name, value in self.file_attrs.items(): attrdef = ','.join([name, *map(str, value)]) diff --git a/gerbonara/utils.py b/gerbonara/utils.py index 53f6398..32954bd 100644 --- a/gerbonara/utils.py +++ b/gerbonara/utils.py @@ -442,7 +442,7 @@ def svg_arc(old, new, center, clockwise): :rtype: str """ - r = math.hypot(*center) + r = float(math.hypot(*center)) # invert sweep flag since the svg y axis is mirrored sweep_flag = int(not clockwise) # In the degenerate case where old == new, we always take the long way around. To represent this "full-circle arc" @@ -451,13 +451,13 @@ def svg_arc(old, new, center, clockwise): intermediate = old[0] + 2*center[0], old[1] + 2*center[1] # Note that we have to preserve the sweep flag to avoid causing self-intersections by flipping the direction of # a circular cutin - return f'A {r:.6} {r:.6} 0 1 {sweep_flag} {intermediate[0]:.6} {intermediate[1]:.6} ' +\ - f'A {r:.6} {r:.6} 0 1 {sweep_flag} {new[0]:.6} {new[1]:.6}' + return f'A {r:.6} {r:.6} 0 1 {sweep_flag} {float(intermediate[0]):.6} {float(intermediate[1]):.6} ' +\ + f'A {r:.6} {r:.6} 0 1 {sweep_flag} {float(new[0]):.6} {float(new[1]):.6}' else: # normal case d = point_line_distance(old, new, (old[0]+center[0], old[1]+center[1])) large_arc = int((d < 0) == clockwise) - return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {new[0]:.6} {new[1]:.6}' + return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {float(new[0]):.6} {float(new[1]):.6}' def svg_rotation(angle_rad, cx=0, cy=0): @@ -525,3 +525,13 @@ def point_in_polygon(point, poly): return res + +def bbox_intersect(a, b): + (xa_min, ya_min), (xa_max, ya_max) = a + (xb_min, yb_min), (xb_mbx, yb_mbx) = b + + x_overlap = not (xa_max < xb_min or xb_max < xa_min) + y_overlap = not (ya_max < yb_min or yb_max < ya_min) + + return x_overlap and y_overlap + -- cgit