summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-04-04 19:06:04 +0200
committerjaseg <git@jaseg.de>2023-04-04 19:06:04 +0200
commit82fcc2445623ff0323692f8253cb81302dc9253a (patch)
tree85f575cc130df2e0235852c492fea79d43bb9904
parenta8772612568a81d204705cb8b4a43bf5868cee40 (diff)
downloadgerbonara-82fcc2445623ff0323692f8253cb81302dc9253a.tar.gz
gerbonara-82fcc2445623ff0323692f8253cb81302dc9253a.tar.bz2
gerbonara-82fcc2445623ff0323692f8253cb81302dc9253a.zip
Various convenience improvements, and make board name guessing really smart
-rw-r--r--gerbonara/cam.py6
-rw-r--r--gerbonara/graphic_primitives.py4
-rw-r--r--gerbonara/layers.py46
-rw-r--r--gerbonara/rs274x.py5
-rw-r--r--gerbonara/utils.py18
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
+