summaryrefslogtreecommitdiff
path: root/gerbonara
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2022-02-04 22:10:19 +0100
committerjaseg <git@jaseg.de>2022-02-04 22:10:19 +0100
commit4cbda84aa61158c06acc78aac4b318bbea5b6214 (patch)
tree7b5724b019324bbadd862076ece399a73a670b90 /gerbonara
parenteaf4f21ce65081da0490a41ee1829b4ec8319109 (diff)
downloadgerbonara-4cbda84aa61158c06acc78aac4b318bbea5b6214.tar.gz
gerbonara-4cbda84aa61158c06acc78aac4b318bbea5b6214.tar.bz2
gerbonara-4cbda84aa61158c06acc78aac4b318bbea5b6214.zip
More doc, fix tests
Diffstat (limited to 'gerbonara')
-rw-r--r--gerbonara/apertures.py4
-rw-r--r--gerbonara/cam.py36
-rwxr-xr-xgerbonara/excellon.py8
-rw-r--r--gerbonara/graphic_objects.py34
-rw-r--r--gerbonara/graphic_primitives.py20
-rw-r--r--gerbonara/ipc356.py2
-rw-r--r--gerbonara/layer_rules.py12
-rw-r--r--gerbonara/layers.py28
-rw-r--r--gerbonara/rs274x.py9
-rw-r--r--gerbonara/tests/test_excellon.py6
-rw-r--r--gerbonara/tests/test_layers.py16
-rw-r--r--gerbonara/utils.py2
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<num>[0-9]+)', fn)):
+ elif (m := re.search(r'(la?y?e?r?|in(ner)?|conduct(or|ive)?)\W*(?P<num>[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. """