diff options
author | jaseg <git@jaseg.de> | 2023-04-04 19:06:04 +0200 |
---|---|---|
committer | jaseg <git@jaseg.de> | 2023-04-04 19:06:04 +0200 |
commit | 82fcc2445623ff0323692f8253cb81302dc9253a (patch) | |
tree | 85f575cc130df2e0235852c492fea79d43bb9904 /gerbonara | |
parent | a8772612568a81d204705cb8b4a43bf5868cee40 (diff) | |
download | gerbonara-82fcc2445623ff0323692f8253cb81302dc9253a.tar.gz gerbonara-82fcc2445623ff0323692f8253cb81302dc9253a.tar.bz2 gerbonara-82fcc2445623ff0323692f8253cb81302dc9253a.zip |
Various convenience improvements, and make board name guessing really smart
Diffstat (limited to 'gerbonara')
-rw-r--r-- | gerbonara/cam.py | 6 | ||||
-rw-r--r-- | gerbonara/graphic_primitives.py | 4 | ||||
-rw-r--r-- | gerbonara/layers.py | 46 | ||||
-rw-r--r-- | gerbonara/rs274x.py | 5 | ||||
-rw-r--r-- | 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 + |