From a1de37d83f22a883f5e4605e59b82aea69b4a563 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 24 Apr 2022 20:08:14 +0200 Subject: Add SVG export to more things --- gerbonara/cam.py | 77 +++++++++++++++-------------------------------------- gerbonara/layers.py | 46 ++++++++++++++++++++++++++++++++ gerbonara/utils.py | 70 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 133 insertions(+), 60 deletions(-) diff --git a/gerbonara/cam.py b/gerbonara/cam.py index 2edf8ed..27b49ce 100644 --- a/gerbonara/cam.py +++ b/gerbonara/cam.py @@ -23,7 +23,7 @@ from copy import deepcopy from enum import Enum import string -from .utils import LengthUnit, MM, Inch, Tag +from .utils import LengthUnit, MM, Inch, Tag, sum_bounds, setup_svg from . import graphic_primitives as gp from . import graphic_objects as go @@ -247,33 +247,23 @@ class CamFile: self.import_settings = import_settings def to_svg(self, margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, fg='black', bg='white', tag=Tag): - - if force_bounds is None: - (min_x, min_y), (max_x, max_y) = self.bounding_box(svg_unit, default=((0, 0), (0, 0))) + if force_bounds: + bounds = svg_unit.convert_bounds_from(arg_unit, force_bounds) else: - (min_x, min_y), (max_x, max_y) = force_bounds - min_x = svg_unit(min_x, arg_unit) - min_y = svg_unit(min_y, arg_unit) - max_x = svg_unit(max_x, arg_unit) - max_y = svg_unit(max_y, arg_unit) - - content_min_x, content_min_y = min_x, min_y - content_w, content_h = max_x - min_x, max_y - min_y - if margin: - margin = svg_unit(margin, arg_unit) - min_x -= margin - min_y -= margin - max_x += margin - max_y += margin - - w, h = max_x - min_x, max_y - min_y - w = 1.0 if math.isclose(w, 0.0) else w - h = 1.0 if math.isclose(h, 0.0) else h - - view = tag('sodipodi:namedview', [], id='namedview1', pagecolor=bg, - inkscape__document_units=svg_unit.shorthand) - - tags = [] + bounds = self.bounding_box(svg_unit, default=((0, 0), (0, 0))) + + tags = list(self.svg_objects(svg_unit=svg_unit, tag=tag, fg=fg, bg=bg)) + + # 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})' + tags = [tag('g', tags, transform=xform)] + + return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, + pagecolor=bg, tag=tag) + + def svg_objects(self, svg_unit=MM, fg='black', bg='white', tag=Tag): pl = None for i, obj in enumerate(self.objects): #if isinstance(obj, go.Flash): @@ -294,29 +284,15 @@ class CamFile: pl = Polyline(primitive) else: if not pl.append(primitive): - tags.append(pl.to_svg(fg, bg, tag=tag)) + yield pl.to_svg(fg, bg, tag=tag) pl = Polyline(primitive) else: if pl: - tags.append(pl.to_svg(fg, bg, tag=tag)) + yield pl.to_svg(fg, bg, tag=tag) pl = None - tags.append(primitive.to_svg(fg, bg, tag=tag)) + yield primitive.to_svg(fg, bg, tag=tag) if pl: - tags.append(pl.to_svg(fg, bg, tag=tag)) - - # setup viewport transform flipping y axis - xform = f'translate({content_min_x} {content_min_y+content_h}) scale(1 -1) translate({-content_min_x} {-content_min_y})' - - svg_unit = 'in' if svg_unit == 'inch' else 'mm' - # TODO export apertures as where reasonable. - return tag('svg', [view, tag('g', tags, transform=xform)], - width=f'{w}{svg_unit}', height=f'{h}{svg_unit}', - viewBox=f'{min_x} {min_y} {w} {h}', - xmlns="http://www.w3.org/2000/svg", - xmlns__xlink="http://www.w3.org/1999/xlink", - xmlns__sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', - xmlns__inkscape='http://www.inkscape.org/namespaces/inkscape', - root=True) + yield pl.to_svg(fg, bg, tag=tag) def size(self, unit=MM): """ Get the dimensions of the file's axis-aligned bounding box, i.e. the difference in x- and y-direction @@ -342,16 +318,7 @@ class CamFile: :rtype: tuple """ - bounds = [ p.bounding_box(unit) for p in self.objects ] - if not bounds: - return default - - min_x = min(x0 for (x0, y0), (x1, y1) in bounds) - min_y = min(y0 for (x0, y0), (x1, y1) in bounds) - max_x = max(x1 for (x0, y0), (x1, y1) in bounds) - max_y = max(y1 for (x0, y0), (x1, y1) in bounds) - - return ((min_x, min_y), (max_x, max_y)) + return sum_bounds(( p.bounding_box(unit) for p in self.objects ), default=default) def to_excellon(self): """ Convert to a :py:class:`.ExcellonFile`. Returns ``self`` if it already is one. """ diff --git a/gerbonara/layers.py b/gerbonara/layers.py index 4591f93..b79f474 100644 --- a/gerbonara/layers.py +++ b/gerbonara/layers.py @@ -29,6 +29,7 @@ from .rs274x import GerberFile from .ipc356 import Netlist from .cam import FileSettings from .layer_rules import MATCH_RULES +from .utils import sum_bounds, setup_svg, MM, Tag STANDARD_LAYERS = [ @@ -409,6 +410,51 @@ class LayerStack: def __repr__(self): return str(self) + def to_svg(self, margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, tag=Tag): + if force_bounds: + bounds = svg_unit.convert_bounds_from(arg_unit, force_bounds) + else: + bounds = self.bounding_box(svg_unit, default=((0, 0), (0, 0))) + + tags = [] + for (side, use), layer in self.graphic_layers.items(): + tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg='black', bg="white", tag=Tag)), + id=f'l-{side}-{use}')) + + for i, layer in enumerate(self.drill_layers): + 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=bg, tag=tag) + + def to_pretty_svg(self, side='top', margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, tag=Tag): + if force_bounds: + bounds = svg_unit.convert_bounds_from(arg_unit, force_bounds) + else: + bounds = self.bounding_box(svg_unit, default=((0, 0), (0, 0))) + + tags = [] + + for use, color in {'copper': 'black', 'mask': 'blue', 'silk': 'red'}: + if (side, use) not in self: + continue + + layer = self[(side, use)] + tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg=color, bg="white", tag=Tag)), + id=f'l-{side}-{use}')) + + for i, layer in enumerate(self.drill_layers): + tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg='magenta', 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=bg, tag=tag) + + + + def bounding_box(self, unit=MM, default=None): + return sum_bounds(( layer.bounding_box(unit, default=default) + for layer in (self.graphic_layers + self.drill_layers) ), default=default) + def merge_drill_layers(self): target = ExcellonFile(comments='Drill files merged by gerbonara') diff --git a/gerbonara/utils.py b/gerbonara/utils.py index bc571b6..db5ccca 100644 --- a/gerbonara/utils.py +++ b/gerbonara/utils.py @@ -95,6 +95,19 @@ class LengthUnit: return unit.convert_from(self, value) + def convert_bounds_from(self, unit, value): + """ :py:meth:`.LengthUnit.convert_from` but for ((min_x, min_y), (max_x, max_y)) bounding box tuples. """ + + if value is None: + return None + + (min_x, min_y), (max_x, max_y) = value + min_x = self.convert_from(unit, min_x) + min_y = self.convert_from(unit, min_y) + max_x = self.convert_from(unit, max_x) + max_y = self.convert_from(unit, max_y) + return (min_x, min_y), (max_x, max_y) + def format(self, value): """ Return a human-readdable string representing value in this unit. @@ -235,7 +248,7 @@ def max_none(a, b): def add_bounds(b1, b2): - """ Add/union two bounding boxes. + """ Add/union multiple bounding boxes. :param tuple b1: ``((min_x, min_y), (max_x, max_y))`` :param tuple b2: ``((min_x, min_y), (max_x, max_y))`` @@ -244,10 +257,28 @@ def add_bounds(b1, b2): :rtype: tuple """ - (min_x_1, min_y_1), (max_x_1, max_y_1) = b1 - (min_x_2, min_y_2), (max_x_2, max_y_2) = b2 - min_x, min_y = min_none(min_x_1, min_x_2), min_none(min_y_1, min_y_2) - max_x, max_y = max_none(max_x_1, max_x_2), max_none(max_y_1, max_y_2) + return sum_bounds((b1, b2)) + + +def sum_bounds(bounds, *, default=None): + """ Add/union multiple bounding boxes. + + :param bounds: each arg is one bounding box in ``((min_x, min_y), (max_x, max_y))`` format + + :returns: ``((min_x, min_y), (max_x, max_y))`` + :rtype: tuple + """ + + if not bounds: + return default + + ((min_x, min_y), (max_x, max_y)), *bounds = bounds + + 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) + max_x, max_y = max_none(max_x, max_x_2), max_none(max_y, max_y_2) + print('sum_bounds +{', (min_x_2, min_y_2), (max_x_2, max_y_2), '} = ', ((min_x, min_y), (max_x, max_y))) + return ((min_x, min_y), (max_x, max_y)) @@ -400,3 +431,32 @@ def svg_arc(old, new, center, clockwise): def svg_rotation(angle_rad, cx=0, cy=0): return f'rotate({float(math.degrees(angle_rad)):.4} {float(cx):.6} {float(cy):.6})' +def setup_svg(tags, bounds, margin=0, arg_unit=MM, svg_unit=MM, pagecolor='white', tag=Tag): + (min_x, min_y), (max_x, max_y) = bounds + + if margin: + margin = svg_unit(margin, arg_unit) + min_x -= margin + min_y -= margin + max_x += margin + max_y += margin + print(f'setting up document {bounds=} {margin=}') + + w, h = max_x - min_x, max_y - min_y + w = 1.0 if math.isclose(w, 0.0) else w + h = 1.0 if math.isclose(h, 0.0) else h + + view = tag('sodipodi:namedview', [], id='namedview1', pagecolor=pagecolor, + inkscape__document_units=svg_unit.shorthand) + + svg_unit = 'in' if svg_unit == 'inch' else 'mm' + # TODO export apertures as where reasonable. + return tag('svg', [view, *tags], + width=f'{w}{svg_unit}', height=f'{h}{svg_unit}', + viewBox=f'{min_x} {min_y} {w} {h}', + xmlns="http://www.w3.org/2000/svg", + xmlns__xlink="http://www.w3.org/1999/xlink", + xmlns__sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + xmlns__inkscape='http://www.inkscape.org/namespaces/inkscape', + root=True) + -- cgit