Various convenience improvements, and make board name guessing really smart

This commit is contained in:
jaseg 2023-04-04 19:06:04 +02:00
parent a877261256
commit 82fcc24456
5 changed files with 59 additions and 20 deletions

View file

@ -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,

View file

@ -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)

View file

@ -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):

View file

@ -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)])

View file

@ -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