More doc, fix tests

This commit is contained in:
jaseg 2022-02-04 22:10:19 +01:00
parent eaf4f21ce6
commit 4cbda84aa6
12 changed files with 78 additions and 99 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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