From 4cbda84aa61158c06acc78aac4b318bbea5b6214 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 4 Feb 2022 22:10:19 +0100 Subject: More doc, fix tests --- gerbonara/apertures.py | 4 ++-- gerbonara/cam.py | 36 ++++++++++++++++++------------------ gerbonara/excellon.py | 8 -------- gerbonara/graphic_objects.py | 34 ++++++++++++++++------------------ gerbonara/graphic_primitives.py | 20 ++++++++++---------- gerbonara/ipc356.py | 2 -- gerbonara/layer_rules.py | 12 ++++++++++-- gerbonara/layers.py | 28 ++++++++++++++++++++-------- gerbonara/rs274x.py | 9 +-------- gerbonara/tests/test_excellon.py | 6 ------ gerbonara/tests/test_layers.py | 16 ---------------- gerbonara/utils.py | 2 +- 12 files changed, 78 insertions(+), 99 deletions(-) diff --git a/gerbonara/apertures.py b/gerbonara/apertures.py index ec14f16..33b78df 100644 --- a/gerbonara/apertures.py +++ b/gerbonara/apertures.py @@ -404,7 +404,7 @@ class PolygonAperture(Aperture): def to_macro(self): return ApertureMacroInstance(GenericMacros.polygon, self._params(MM)) - def params(self, unit=None): + def _params(self, unit=None): rotation = self.rotation % (2*math.pi / self.n_vertices) if self.rotation is not None else None if self.hole_dia is not None: return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation, self.unit.convert_to(unit, self.hole_dia) @@ -457,7 +457,7 @@ class ApertureMacroInstance(Aperture): hasattr(other, 'params') and self.params == other.params and \ hasattr(other, 'rotation') and self.rotation == other.rotation - def params(self, unit=None): + def _params(self, unit=None): # We ignore "unit" here as we convert the actual macro, not this instantiation. # We do this because here we do not have information about which parameter has which physical units. return tuple(self.parameters) diff --git a/gerbonara/cam.py b/gerbonara/cam.py index a96a1eb..ef99dfc 100644 --- a/gerbonara/cam.py +++ b/gerbonara/cam.py @@ -125,7 +125,7 @@ class FileSettings: @property def is_absolute(self): - return not self.incremental # default to absolute + return not self.is_incremental # default to absolute def parse_gerber_value(self, value): """ Parse a numeric string in gerber format using this file's settings. """ @@ -220,7 +220,7 @@ class Polyline: self.append(line) def append(self, line): - assert isinstance(line, Line) + assert isinstance(line, gp.Line) if not self.coords: self.coords.append((line.x1, line.y1)) self.coords.append((line.x2, line.y2)) @@ -287,12 +287,12 @@ class CamFile: inkscape__document_units=svg_unit.shorthand) tags = [] - polyline = None + pl = None for i, obj in enumerate(self.objects): #if isinstance(obj, go.Flash): - # if polyline: - # tags.append(polyline.to_svg(tag, fg, bg)) - # polyline = None + # if pl: + # tags.append(pl.to_svg(tag, fg, bg)) + # pl = None # mask_tags = [ prim.to_svg(tag, 'white', 'black') for prim in obj.to_primitives(unit=svg_unit) ] # mask_tags.insert(0, tag('rect', width='100%', height='100%', fill='black')) @@ -303,19 +303,19 @@ class CamFile: #else: for primitive in obj.to_primitives(unit=svg_unit): if isinstance(primitive, gp.Line): - if not polyline: - polyline = gp.Polyline(primitive) + if not pl: + pl = Polyline(primitive) else: - if not polyline.append(primitive): - tags.append(polyline.to_svg(fg, bg, tag=tag)) - polyline = gp.Polyline(primitive) + if not pl.append(primitive): + tags.append(pl.to_svg(fg, bg, tag=tag)) + pl = Polyline(primitive) else: - if polyline: - tags.append(polyline.to_svg(fg, bg, tag=tag)) - polyline = None + if pl: + tags.append(pl.to_svg(fg, bg, tag=tag)) + pl = None tags.append(primitive.to_svg(fg, bg, tag=tag)) - if polyline: - tags.append(polyline.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})' @@ -421,7 +421,7 @@ class CamFile: @property def is_empty(self): """ Check if there are any objects in this file. """ - raise NotImplementedError() + return not bool(list(self.objects)) def __len__(self): """ Return the number of objects in this file. Note that a e.g. a long trace or a long slot consisting of @@ -430,5 +430,5 @@ class CamFile: def __bool__(self): """ Test if this file contains any objects """ - raise NotImplementedError() + return not self.is_empty diff --git a/gerbonara/excellon.py b/gerbonara/excellon.py index 575e4a2..8b8744f 100755 --- a/gerbonara/excellon.py +++ b/gerbonara/excellon.py @@ -196,9 +196,6 @@ class ExcellonFile(CamFile): def __repr__(self): return str(self) - def __bool__(self): - return not self.is_empty - @property def is_plated(self): """ Test if *all* holes or slots in this file are plated. """ @@ -385,10 +382,6 @@ class ExcellonFile(CamFile): for obj in self.objects: obj.rotate(angle, cx, cy, unit=unit) - @property - def is_empty(self): - return not self.objects - def __len__(self): return len(self.objects) @@ -540,7 +533,6 @@ class ExcellonParser(object): # TODO check first command in file is "start of header" command. try: - print(f'{self.settings.number_format} {lineno} "{line}"') if not self.exprs.handle(self, line): raise ValueError('Unknown excellon statement:', line) except Exception as e: diff --git a/gerbonara/graphic_objects.py b/gerbonara/graphic_objects.py index cf4260f..97a39c8 100644 --- a/gerbonara/graphic_objects.py +++ b/gerbonara/graphic_objects.py @@ -68,8 +68,9 @@ class GraphicObject: :returns: A copy of this object using the new unit. """ - copy = copy.copy(self) - copy.convert_to(unit) + obj = copy.copy(self) + obj.convert_to(unit) + return obj def convert_to(self, unit): """ Convert this gerber object to another :py:class:`.LengthUnit` in-place. @@ -140,9 +141,8 @@ class GraphicObject: :rtype: Iterator[:py:class:`.GraphicPrimitive`] """ - return self._to_primitives(unit) - def _to_statements(self, gs): + def to_statements(self, gs): """ Serialize this object into Gerber statements. :param gs: :py:class:`~.rs274x.GraphicsState` object containing current Gerber state (polarity, selected @@ -151,9 +151,8 @@ class GraphicObject: :returns: Iterator yielding one string per line of output Gerber :rtype: Iterator[str] """ - self._to_statements(gs) - def _to_xnc(self, ctx): + def to_xnc(self, ctx): """ Serialize this object into XNC Excellon statements. :param ctx: :py:class:`.ExcellonContext` object containing current Excellon state (selected tool, @@ -162,7 +161,6 @@ class GraphicObject: :returns: Iterator yielding one string per line of output XNC code :rtype: Iterator[str] """ - self._to_xnc(ctx) @dataclass @@ -200,18 +198,18 @@ class Flash(GraphicObject): """ return getattr(self.tool, 'plated', None) - def __offset(self, dx, dy): + def _offset(self, dx, dy): self.x += dx self.y += dy def _rotate(self, rotation, cx=0, cy=0): self.x, self.y = gp.rotate_point(self.x, self.y, rotation, cx, cy) - def _to_primitives(self, unit=None): + def to_primitives(self, unit=None): conv = self.converted(unit) yield from self.aperture.flash(conv.x, conv.y, unit, self.polarity_dark) - def _to_statements(self, gs): + def to_statements(self, gs): yield from gs.set_polarity(self.polarity_dark) yield from gs.set_aperture(self.aperture) @@ -221,7 +219,7 @@ class Flash(GraphicObject): gs.update_point(self.x, self.y, unit=self.unit) - def _to_xnc(self, ctx): + def to_xnc(self, ctx): yield from ctx.select_tool(self.tool) yield from ctx.drill_mode() @@ -290,7 +288,7 @@ class Region(GraphicObject): else: self.poly.arc_centers.append(None) - def _to_primitives(self, unit=None): + def to_primitives(self, unit=None): self.poly.polarity_dark = self.polarity_dark # FIXME: is this the right spot to do this? if unit == self.unit: yield self.poly @@ -402,12 +400,12 @@ class Line(GraphicObject): """ return self.tool.plated - def _to_primitives(self, unit=None): + def to_primitives(self, unit=None): conv = self.converted(unit) w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging yield gp.Line(*conv.p1, *conv.p2, w, polarity_dark=self.polarity_dark) - def _to_statements(self, gs): + def to_statements(self, gs): yield from gs.set_polarity(self.polarity_dark) yield from gs.set_aperture(self.aperture) yield from gs.set_interpolation_mode(InterpMode.LINEAR) @@ -419,7 +417,7 @@ class Line(GraphicObject): gs.update_point(*self.p2, unit=self.unit) - def _to_xnc(self, ctx): + def to_xnc(self, ctx): yield from ctx.select_tool(self.tool) yield from ctx.route_mode(self.unit, *self.p1) @@ -565,7 +563,7 @@ class Arc(GraphicObject): self.x2, self.y2 = gp.rotate_point(self.x2, self.y2, rotation, cx, cy) self.cx, self.cy = new_cx - self.x1, new_cy - self.y1 - def _to_primitives(self, unit=None): + def to_primitives(self, unit=None): conv = self.converted(unit) w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging yield gp.Arc(x1=conv.x1, y1=conv.y1, @@ -575,7 +573,7 @@ class Arc(GraphicObject): width=w, polarity_dark=self.polarity_dark) - def _to_statements(self, gs): + def to_statements(self, gs): yield from gs.set_polarity(self.polarity_dark) yield from gs.set_aperture(self.aperture) # TODO is the following line correct? @@ -590,7 +588,7 @@ class Arc(GraphicObject): gs.update_point(*self.p2, unit=self.unit) - def _to_xnc(self, ctx): + def to_xnc(self, ctx): yield from ctx.select_tool(self.tool) yield from ctx.route_mode(self.unit, self.x1, self.y1) code = 'G02' if self.clockwise else 'G03' diff --git a/gerbonara/graphic_primitives.py b/gerbonara/graphic_primitives.py index 6e785d9..df49327 100644 --- a/gerbonara/graphic_primitives.py +++ b/gerbonara/graphic_primitives.py @@ -104,12 +104,12 @@ class ArcPoly(GraphicPrimitive): def from_regular_polygon(kls, x:float, y:float, r:float, n:int, rotation:float=0, polarity_dark:bool=True): """ Convert an n-sided gerber polygon to a normal ArcPoly defined by outline """ - delta = 2*math.pi / self.n + delta = 2*math.pi / n return kls([ - (self.x + math.cos(self.rotation + i*delta) * self.r, - self.y + math.sin(self.rotation + i*delta) * self.r) - for i in range(self.n) ], polarity_dark=polarity_dark) + (x + math.cos(rotation + i*delta) * r, + y + math.sin(rotation + i*delta) * r) + for i in range(n) ], polarity_dark=polarity_dark) def __len__(self): """ Return the number of points on this polygon's outline (which is also the number of segments because the @@ -156,15 +156,15 @@ class Line(GraphicPrimitive): @classmethod def from_obround(kls, x:float, y:float, w:float, h:float, rotation:float=0, polarity_dark:bool=True): """ Convert a gerber obround into a :py:class:`~.graphic_primitives.Line`. """ - if self.w > self.h: - w, a, b = self.h, self.w-self.h, 0 + if w > h: + w, a, b = h, w-h, 0 else: - w, a, b = self.w, 0, self.h-self.w + w, a, b = w, 0, h-w return kls( - *rotate_point(self.x-a/2, self.y-b/2, self.rotation, self.x, self.y), - *rotate_point(self.x+a/2, self.y+b/2, self.rotation, self.x, self.y), - w, polarity_dark=self.polarity_dark) + *rotate_point(x-a/2, y-b/2, rotation, x, y), + *rotate_point(x+a/2, y+b/2, rotation, x, y), + w, polarity_dark=polarity_dark) def bounding_box(self): r = self.width / 2 diff --git a/gerbonara/ipc356.py b/gerbonara/ipc356.py index 3e6998c..175cb5e 100644 --- a/gerbonara/ipc356.py +++ b/gerbonara/ipc356.py @@ -561,10 +561,8 @@ class Outline: @classmethod def parse(kls, line, settings): - print('parsing outline', line) outline_type = OutlineType[line[3:17].strip()] for outline in parse_coord_chain(line[22:], settings): - print(' ->', outline) yield kls(outline_type, outline, unit=settings.unit) def format(self, settings): diff --git a/gerbonara/layer_rules.py b/gerbonara/layer_rules.py index 7d61170..fc9af7a 100644 --- a/gerbonara/layer_rules.py +++ b/gerbonara/layer_rules.py @@ -148,7 +148,7 @@ MATCH_RULES = { }, 'allegro': { - # Allegro doesn't have any widespread convention, so we rely heavily on the layer name auto-guesser here. + # Allegro doesn't have any widespread convention, so we rely heavily on the layer name auto-guesser here. 'drill mech': r'.*\.rou', 'drill mech': r'.*\.drl', 'generic gerber': r'.*\.art', @@ -160,9 +160,17 @@ MATCH_RULES = { }, 'pads': { - # Pads also does not seem to have a factory-default naming schema. Or it has one but everyone ignores it. + # Pads also does not seem to have a factory-default naming schema. Or it has one but everyone ignores it. 'generic gerber': r'.*\.pho', 'drill mech': r'.*\.drl', }, +'zuken': { + 'generic gerber': r'.*\.fph', + 'gerber params': r'.*\.fpl', + 'drill mech': r'.*\.fdr', + 'excellon params': r'.*\.fdl', + 'other netlist': r'.*\.ipc', + 'ipc-2581': r'.*\.xml', + }, } diff --git a/gerbonara/layers.py b/gerbonara/layers.py index 970b214..eb21c92 100644 --- a/gerbonara/layers.py +++ b/gerbonara/layers.py @@ -58,12 +58,14 @@ def match_files(filenames): gen[target] = gen.get(target, []) + [fn] return matches + def best_match(filenames): matches = match_files(filenames) matches = sorted(matches.items(), key=lambda pair: len(pair[1])) generator, files = matches[-1] return generator, files + def identify_file(data): if 'M48' in data: return 'excellon' @@ -79,6 +81,7 @@ def identify_file(data): return None + def common_prefix(l): out = [] for cand in l: @@ -115,6 +118,7 @@ def autoguess(filenames): return matches + def layername_autoguesser(fn): fn, _, ext = fn.lower().rpartition('.') @@ -125,6 +129,7 @@ def layername_autoguesser(fn): if re.search('top|front|pri?m?(ary)?', fn): side = 'top' use = 'copper' + if re.search('bot(tom)?|back|sec(ondary)?', fn): side = 'bottom' use = 'copper' @@ -135,20 +140,20 @@ def layername_autoguesser(fn): elif re.search('(solder)?paste', fn): use = 'paste' - elif re.search('(solder)?mask', fn): + elif re.search('(solder)?(mask|resist)', fn): use = 'mask' elif re.search('drill|rout?e?', fn): use = 'drill' side = 'unknown' - if re.search(r'np(th)?|(non|un)\W*plated|(non|un)\Wgalv', fn): + if re.search(r'np(th|lt)?|(non|un)\W*plated|(non|un)\Wgalv', fn): side = 'nonplated' - elif re.search('pth|plated|galv', fn): + elif re.search('pth|plated|galv|plt', fn): side = 'plated' - elif (m := re.search(r'(la?y?e?r?|in(ner)?)\W*(?P[0-9]+)', fn)): + elif (m := re.search(r'(la?y?e?r?|in(ner)?|conduct(or|ive)?)\W*(?P[0-9]+)', fn)): use = 'copper' side = f'inner_{int(m["num"]):02d}' @@ -169,6 +174,7 @@ def layername_autoguesser(fn): return f'{side} {use}' + class LayerStack: @classmethod def from_directory(kls, directory, board_name=None, verbose=False): @@ -179,7 +185,7 @@ class LayerStack: files = [ path for path in directory.glob('**/*') if path.is_file() ] generator, filemap = best_match(files) - print('detected generator', generator) + #print('detected generator', generator) if len(filemap) < 6: warnings.warn('Ambiguous gerber filenames. Trying last-resort autoguesser.') @@ -210,6 +216,12 @@ class LayerStack: raise SystemError('Cannot figure out gerber file mapping') # FIXME use layer metadata from comments and ipc file if available + elif generator == 'zuken': + filemap = autoguess([ f for files in filemap for f in files ]) + if len(filemap < 6): + raise SystemError('Cannot figure out gerber file mapping') + # FIXME use layer metadata from comments and ipc file if available + elif generator == 'altium': excellon_settings = None @@ -231,8 +243,8 @@ class LayerStack: else: excellon_settings = None - import pprint - pprint.pprint(filemap) + #import pprint + #pprint.pprint(filemap) ambiguous = [ key for key, value in filemap.items() if len(value) > 1 and not 'drill' in key ] if ambiguous: @@ -247,7 +259,7 @@ class LayerStack: for path in paths: id_result = identify_file(path.read_text()) - print('id_result', id_result) + #print('id_result', id_result) if 'netlist' in key: layer = Netlist.open(path) diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py index 3dd8bb7..b355bda 100644 --- a/gerbonara/rs274x.py +++ b/gerbonara/rs274x.py @@ -255,20 +255,13 @@ class GerberFile(CamFile): settings.number_format = (5,6) return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)) - @property - def is_empty(self): - return not self.objects - def __len__(self): return len(self.objects) - def __bool__(self): - return not self.is_empty - def offset(self, dx=0, dy=0, unit=MM): # TODO round offset to file resolution for obj in self.objects: - obj.with_offset(dx, dy, unit) + obj.offset(dx, dy, unit) def rotate(self, angle:'radian', center=(0,0), unit=MM): if math.isclose(angle % (2*math.pi), 0): diff --git a/gerbonara/tests/test_excellon.py b/gerbonara/tests/test_excellon.py index 2d2b32a..3b0418d 100644 --- a/gerbonara/tests/test_excellon.py +++ b/gerbonara/tests/test_excellon.py @@ -131,8 +131,6 @@ def test_gerber_alignment(reference, tmpfile, print_on_error): for obj in gerf.objects: if isinstance(obj, Flash): x, y = obj.unit.convert_to(MM, obj.x), obj.unit.convert_to(MM, obj.y) - if abs(x - 121.525) < 2 and abs(y - 64) < 2: - print(obj) flash_coords.append((x, y)) tree = KDTree(flash_coords, copy_data=True) @@ -144,10 +142,6 @@ def test_gerber_alignment(reference, tmpfile, print_on_error): if obj.plated in (True, None): total += 1 x, y = obj.unit.convert_to(MM, obj.x), obj.unit.convert_to(MM, obj.y) - print((x, y), end=' ') - if abs(x - 121.525) < 2 and abs(y - 64) < 2: - print(obj) - print(' ', tree.query_ball_point((x, y), r=tolerance)) if tree.query_ball_point((x, y), r=tolerance): matches += 1 diff --git a/gerbonara/tests/test_layers.py b/gerbonara/tests/test_layers.py index 795af25..c41c02d 100644 --- a/gerbonara/tests/test_layers.py +++ b/gerbonara/tests/test_layers.py @@ -275,22 +275,6 @@ REFERENCE_DIRS = { 'NCDrill/ThruHolePlated.ncd': 'drill plated', }, - 'zuken': { - '': 'mechanical outline', - 'Gerber/DrillDrawingThrough.gdo': None, - 'Gerber/EtchLayerBottom.gdo': 'bottom copper', - 'Gerber/EtchLayerTop.gdo': 'top copper', - 'Gerber/GerberPlot.gpf': None, - 'Gerber/PCB.dsn': None, - 'Gerber/SolderPasteBottom.gdo': 'bottom paste', - 'Gerber/SolderPasteTop.gdo': 'top paste', - 'Gerber/SoldermaskBottom.gdo': 'bottom mask', - 'Gerber/SoldermaskTop.gdo': 'top mask', - 'NCDrill/ContourPlated.ncd': 'mechanical outline', - 'NCDrill/ThruHoleNonPlated.ncd': 'drill nonplated', - 'NCDrill/ThruHolePlated.ncd': 'drill plated', - }, - 'upverter': { 'design_export.drl': 'drill unknown', 'design_export.gbl': 'bottom copper', diff --git a/gerbonara/utils.py b/gerbonara/utils.py index 1a92116..bc571b6 100644 --- a/gerbonara/utils.py +++ b/gerbonara/utils.py @@ -29,7 +29,7 @@ import os import re import textwrap from enum import Enum -from math import radians, sin, cos, sqrt, atan2, pi +import math class UnknownStatementWarning(Warning): """ Gerbonara found an unknown Gerber or Excellon statement. """ -- cgit