More doc, fix tests
This commit is contained in:
parent
eaf4f21ce6
commit
4cbda84aa6
12 changed files with 78 additions and 99 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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. """
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue