Fix layer stack SVG export
This commit is contained in:
parent
c127c89fa3
commit
e422243a6e
6 changed files with 86 additions and 21 deletions
|
|
@ -22,6 +22,9 @@ from dataclasses import dataclass
|
|||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
import string
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from functools import cached_property
|
||||
|
||||
from .utils import LengthUnit, MM, Inch, Tag, sum_bounds, setup_svg
|
||||
from . import graphic_primitives as gp
|
||||
|
|
@ -246,6 +249,14 @@ class CamFile:
|
|||
self.layer_name = layer_name
|
||||
self.import_settings = import_settings
|
||||
|
||||
@property
|
||||
def is_lazy(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def instance(self):
|
||||
return self
|
||||
|
||||
def to_svg(self, margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, fg='black', bg='white', tag=Tag):
|
||||
if force_bounds:
|
||||
bounds = svg_unit.convert_bounds_from(arg_unit, force_bounds)
|
||||
|
|
@ -386,3 +397,22 @@ class CamFile:
|
|||
""" Test if this file contains any objects """
|
||||
return not self.is_empty
|
||||
|
||||
class LazyCamFile:
|
||||
def __init__(self, klass, path, *args, **kwargs):
|
||||
self._class = klass
|
||||
self.original_path = Path(path)
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
@cached_property
|
||||
def instance(self):
|
||||
return self._class.open(self.original_path, *self._args, **self._kwargs)
|
||||
|
||||
@property
|
||||
def is_lazy(self):
|
||||
return True
|
||||
|
||||
def save(self, filename, *args, **kwargs):
|
||||
""" Copy this Gerber file to the new path. """
|
||||
shutil.copy(self.original_path, filename)
|
||||
|
||||
|
|
|
|||
|
|
@ -207,13 +207,14 @@ class Arc(GraphicPrimitive):
|
|||
dx, dy = self.x1 - self.cx, self.y1 - self.cy
|
||||
x1 = self.x1 + dx/arc_r * r
|
||||
y1 = self.y1 + dy/arc_r * r
|
||||
|
||||
|
||||
# same for C -> P2
|
||||
dx, dy = self.x2 - self.cx, self.y2 - self.cy
|
||||
x2 = self.x2 + dx/arc_r * r
|
||||
y2 = self.y2 + dy/arc_r * r
|
||||
|
||||
arc = arc_bounds(x1, y1, x2, y2, self.cx, self.cy, self.clockwise)
|
||||
|
||||
return add_bounds(endpoints, arc) # FIXME add "include_center" switch
|
||||
|
||||
def to_svg(self, fg='black', bg='white', tag=Tag):
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ MATCH_RULES = {
|
|||
'kicad': {
|
||||
'top copper': r'.*\.gtl|.*f.cu.(gbr|gtl)',
|
||||
'top mask': r'.*\.gts|.*f.mask.(gbr|gts)',
|
||||
'top silk': r'.*\.gto|.*f.silks.(gbr|gto)',
|
||||
'top silk': r'.*\.gto|.*f.silks(creen)?.(gbr|gto)',
|
||||
'top paste': r'.*\.gtp|.*f.paste.(gbr|gtp)',
|
||||
'bottom copper': r'.*\.gbl|.*b.cu.(gbr|gbl)',
|
||||
'bottom mask': r'.*\.gbs|.*b.mask.(gbr|gbs)',
|
||||
'bottom silk': r'.*\.gbo|.*b.silks.(gbr|gbo)',
|
||||
'bottom silk': r'.*\.gbo|.*b.silks(creen)?.(gbr|gbo)',
|
||||
'bottom paste': r'.*\.gbp|.*b.paste.(gbr|gbp)',
|
||||
'inner copper': r'.*\.gp?([0-9]+)|.*inn?e?r?([0-9]+).cu.(?:gbr|g[0-9]+)',
|
||||
'mechanical outline': r'.*\.(gm[0-9]+)|.*edge.cuts.(gbr|gm1)',
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ import os
|
|||
import re
|
||||
import warnings
|
||||
import copy
|
||||
import itertools
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
|
||||
from .excellon import ExcellonFile, parse_allegro_ncparam, parse_allegro_logfile
|
||||
from .rs274x import GerberFile
|
||||
from .ipc356 import Netlist
|
||||
from .cam import FileSettings
|
||||
from .cam import FileSettings, LazyCamFile
|
||||
from .layer_rules import MATCH_RULES
|
||||
from .utils import sum_bounds, setup_svg, MM, Tag
|
||||
|
||||
|
|
@ -208,13 +210,40 @@ class LayerStack:
|
|||
self.netlist = netlist
|
||||
|
||||
@classmethod
|
||||
def from_directory(kls, directory, board_name=None):
|
||||
def open(kls, path, board_name=None, lazy=False):
|
||||
path = Path(path)
|
||||
if path.is_dir():
|
||||
return kls.from_directory(path, board_name=board_name, lazy=lazy)
|
||||
elif path.suffix.lower() == '.zip' or is_zipfile(path):
|
||||
return kls.from_zipfile(path, board_name=board_name, lazy=lazy)
|
||||
else:
|
||||
return kls.from_files([path], board_name=board_name, lazy=lazy)
|
||||
|
||||
@classmethod
|
||||
def from_zipfile(kls, filename, board_name=None, lazy=False):
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
tmp_indir = Path(tmpdir) / dirname
|
||||
tmp_indir.mkdir()
|
||||
|
||||
with ZipFile(source) as f:
|
||||
f.extractall(path=tmp_indir)
|
||||
|
||||
inst = kls.from_directory(tmp_indir, board_name=board_name, lazy=lazy)
|
||||
inst.tmpdir = tmpdir
|
||||
return inst
|
||||
|
||||
@classmethod
|
||||
def from_directory(kls, directory, board_name=None, lazy=False):
|
||||
|
||||
directory = Path(directory)
|
||||
if not directory.is_dir():
|
||||
raise FileNotFoundError(f'{directory} is not a directory')
|
||||
|
||||
files = [ path for path in directory.glob('**/*') if path.is_file() ]
|
||||
return kls.from_files(files, board_name=board_name, lazy=lazy)
|
||||
|
||||
@classmethod
|
||||
def from_files(kls, files, board_name=None, lazy=False):
|
||||
generator, filemap = best_match(files)
|
||||
|
||||
if sum(len(files) for files in filemap.values()) < 6:
|
||||
|
|
@ -290,7 +319,7 @@ class LayerStack:
|
|||
id_result = identify_file(path.read_text())
|
||||
|
||||
if 'netlist' in key:
|
||||
layer = Netlist.open(path)
|
||||
layer = LazyCamFile(Netlist, path)
|
||||
|
||||
elif ('outline' in key or 'drill' in key) and id_result != 'gerber':
|
||||
if id_result is None:
|
||||
|
|
@ -304,10 +333,13 @@ class LayerStack:
|
|||
plated = True
|
||||
else:
|
||||
plated = None
|
||||
layer = ExcellonFile.open(path, plated=plated, settings=excellon_settings, external_tools=external_tools)
|
||||
layer = LazyCamFile(ExcellonFile, path, plated=plated, settings=excellon_settings, external_tools=external_tools)
|
||||
else:
|
||||
|
||||
layer = GerberFile.open(path)
|
||||
layer = LazyCamFile(GerberFile, path)
|
||||
|
||||
if not lazy:
|
||||
layer = layer.open()
|
||||
|
||||
if key == 'mechanical outline':
|
||||
layers['mechanical', 'outline'] = layer
|
||||
|
|
@ -325,10 +357,11 @@ class LayerStack:
|
|||
side, _, use = key.partition(' ')
|
||||
layers[(side, use)] = layer
|
||||
|
||||
hints = set(layer.generator_hints) | { generator }
|
||||
if len(hints) > 1:
|
||||
warnings.warn('File identification returned ambiguous results. Please raise an issue on the gerbonara '
|
||||
'tracker and if possible please provide these input files for reference.')
|
||||
if not lazy:
|
||||
hints = set(layer.generator_hints) | { generator }
|
||||
if len(hints) > 1:
|
||||
warnings.warn('File identification returned ambiguous results. Please raise an issue on the '
|
||||
'gerbonara tracker and if possible please provide these input files for reference.')
|
||||
|
||||
board_name = common_prefix([l.original_path.name for l in layers.values() if l is not None])
|
||||
board_name = re.sub(r'^\W+', '', board_name)
|
||||
|
|
@ -431,29 +464,30 @@ class LayerStack:
|
|||
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)))
|
||||
bounds = self.outline.instance.bounding_box(svg_unit, default=((0, 0), (0, 0)))
|
||||
|
||||
tags = []
|
||||
|
||||
for use, color in {'copper': 'black', 'mask': 'blue', 'silk': 'red'}:
|
||||
for use, color in {'copper': 'black', 'mask': 'blue', 'silk': 'red'}.items():
|
||||
if (side, use) not in self:
|
||||
warnings.warn(f'Layer "{side} {use}" not found. Found layers: {", ".join(side + " " + use for side, use in self.graphic_layers)}')
|
||||
continue
|
||||
|
||||
layer = self[(side, use)]
|
||||
tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg=color, bg="white", tag=Tag)),
|
||||
tags.append(tag('g', list(layer.instance.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}'))
|
||||
tags.append(tag('g', list(layer.instance.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)
|
||||
return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, pagecolor="white", 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)
|
||||
for layer in itertools.chain(self.graphic_layers.values(), self.drill_layers) ), default=default)
|
||||
|
||||
def merge_drill_layers(self):
|
||||
target = ExcellonFile(comments='Drill files merged by gerbonara')
|
||||
|
|
|
|||
|
|
@ -533,7 +533,7 @@ class GerberParser:
|
|||
'eof': r"M0?[02]",
|
||||
'ignored': r"(?P<stmt>M01)",
|
||||
# NOTE: The official spec says names can be empty or contain commas. I think that doesn't make sense.
|
||||
'attribute': r"(?P<eagle_garbage>G04 #@! %)?(?P<type>TF|TA|TO|TD)(?P<name>[._$a-zA-Z][._$a-zA-Z0-9]*)(,(?P<value>.*))",
|
||||
'attribute': r"(?P<eagle_garbage>G04 #@! %)?(?P<type>TF|TA|TO|TD)(?P<name>[._$a-zA-Z][._$a-zA-Z0-9]*)?(,(?P<value>.*))?",
|
||||
# Eagle file attributes handled above.
|
||||
'comment': r"G0?4(?P<comment>[^*]*)",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,7 +449,7 @@ def test_compositing(file_a, file_b, angle, offset, tmpfile, print_on_error):
|
|||
|
||||
@filter_syntax_warnings
|
||||
@pytest.mark.parametrize('reference', REFERENCE_FILES, indirect=True)
|
||||
def test_svg_export(reference, tmpfile):
|
||||
def test_svg_export_gerber(reference, tmpfile):
|
||||
if reference.name in ('silkscreen_bottom.gbr', 'silkscreen_top.gbr', 'top_silk.GTO'):
|
||||
# Some weird svg rendering artifact. Might be caused by mismatching svg units between gerbv and us. Result looks
|
||||
# fine though.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue