diff --git a/README.rst b/README.rst
index 56e8e76..15a1e74 100644
--- a/README.rst
+++ b/README.rst
@@ -61,7 +61,7 @@ Gerbolyze works in three steps.
.. code::
- $ gerbolyze paste --top template_top_edited.svg [--bottom ...] my_gerber_dir output_gerber_dir
+ $ gerbolyze paste template_top_edited.svg my_gerber_dir output_gerber_dir
Quick Start Installation (Any Platform)
---------------------------------------
diff --git a/gerbolyze/protoboard.py b/gerbolyze/protoboard.py
deleted file mode 100644
index c9c8d00..0000000
--- a/gerbolyze/protoboard.py
+++ /dev/null
@@ -1,636 +0,0 @@
-#!/usr/bin/env python3
-
-import re
-import textwrap
-import ast
-import uuid
-
-svg_str = lambda content: content if isinstance(content, str) else '\n'.join(str(c) for c in content)
-
-class Pattern:
- def __init__(self, w, h=None):
- self.vb_w = self.w = w
- self.vb_h = self.h = h or w
-
- def svg_def(self, svg_id, off_x, off_y):
- return textwrap.dedent(f'''
-
- {svg_str(self.content)}
- ''')
-
-def make_rect(svg_id, x, y, w, h, clip=''):
- #import random
- #c = random.randint(0, 2**24)
- #return f''
- return f''
-
-class CirclePattern(Pattern):
- def __init__(self, d, w, h=None):
- super().__init__(w, h)
- self.d = d
-
- @property
- def content(self):
- return f''
-
-class RectPattern(Pattern):
- def __init__(self, rw, rh, w, h):
- super().__init__(w, h)
- self.rw, self.rh = rw, rh
-
- @property
- def content(self):
- x = (self.w - self.rw) / 2
- y = (self.h - self.rh) / 2
- return f''
-
-class ManhattanPattern(Pattern):
- def __init__(self, pitch=2.54*4, gap=0.2):
- super().__init__(pitch)
- self.vb_w, self.vb_h = 1, 1
- self.gap = gap
-
- @property
- def content(self):
- return textwrap.dedent('''
-
-
-
-
-
- '''.strip())
-
-make_layer = lambda layer_name, content: \
- f'{svg_str(content)}'
-
-svg_template = textwrap.dedent('''
-
-
-''').strip()
-
-class PatternProtoArea:
- def __init__(self, pitch_x, pitch_y=None, border=None):
- self.pitch_x = pitch_x
- self.pitch_y = pitch_y or pitch_x
-
- if border is None:
- self.border = (0, 0, 0, 0)
- elif hasattr(border, '__iter__'):
- if len(border == 4):
- self.border = border
- else:
- raise TypeError('border must be None, int, or a 4-tuple of floats (top, right, bottom, left)')
- else:
- self.border = (border, border, border, border)
-
- @property
- def pitch(self):
- if self.pitch_x != self.pitch_y:
- raise ValueError('Pattern has different X and Y pitches')
- return self.pitch_x
-
- def fit_size(self, w, h):
- x, y, w, h = self.fit_rect(0, 0, w, h, False)
- t, r, b, l = self.border
- return (w+l+r), (h+t+b)
-
- def fit_rect(self, x, y, w, h, center=True):
- t, r, b, l = self.border
- x, y, w, h = (x+l), (y+t), (w-l-r), (h-t-b)
-
- w_mod, h_mod = round((w + 5e-7) % self.pitch_x, 6), round((h + 5e-7) % self.pitch_y, 6)
- w_fit, h_fit = round(w - w_mod, 6), round(h - h_mod, 6)
-
- if center:
- x = x + (w-w_fit)/2
- y = y + (h-h_fit)/2
- return x, y, w_fit, h_fit
-
- else:
- return x, y, w_fit, h_fit
-
- def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
- yield {}
-
- def symmetric_sides(self):
- return False
-
- def used_patterns(self):
- yield self
-
-
-class EmptyProtoArea:
- def __init__(self, copper=False, border=None):
- self.copper = copper
-
- if border is None:
- self.border = (0, 0, 0, 0)
- elif hasattr(border, '__iter__'):
- if len(border == 4):
- self.border = border
- else:
- raise TypeError('border must be None, int, or a 4-tuple of floats (top, right, bottom, left)')
- else:
- self.border = (border, border, border, border)
-
- def fit_size(self, w, h):
- return w, h
-
- def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
- if self.copper:
- t, r, b, l = self.border
- x, y, w, h = x+l, y+t, w-l-r, h-t-b
- yield { 'top copper': f'' }
- else:
- yield {}
-
- def used_patterns(self):
- yield self
-
-
-class THTProtoArea(PatternProtoArea):
- def __init__(self, pad_size=2.0, drill=1.0, pitch=2.54, sides='both', plated=True, border=None, pad_shape='circle'):
- super().__init__(pitch, border=border)
- self.pad_size = pad_size
- self.pad_shape = pad_shape.lower().rstrip('s')
- self.drill = drill
- self.drill_pattern = CirclePattern(self.drill, self.pitch)
- if self.pad_shape == 'circle':
- self.pad_pattern = CirclePattern(self.pad_size, self.pitch)
- elif self.pad_shape == 'square':
- self.pad_pattern = RectPattern(self.pad_size, self.pad_size, self.pitch, self.pitch)
- self.patterns = [self.drill_pattern, self.pad_pattern]
- self.plated = plated
- self.sides = sides
-
- def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
- x, y, w, h = self.fit_rect(x, y, w, h, center)
- drill = 'plated drill' if self.plated else 'nonplated drill'
-
- pad_id = str(uuid.uuid4())
- drill_id = str(uuid.uuid4())
-
- d = { drill: make_rect(drill_id, x, y, w, h, clip),
- 'defs': [
- self.pad_pattern.svg_def(pad_id, x, y),
- self.drill_pattern.svg_def(drill_id, x, y)]}
-
- if self.sides in ('top', 'both'):
- d['top copper'] = make_rect(pad_id, x, y, w, h, clip)
- d['top mask'] = make_rect(pad_id, x, y, w, h, clip)
- if self.sides in ('bottom', 'both'):
- d['bottom copper'] = make_rect(pad_id, x, y, w, h, clip)
- d['bottom mask'] = make_rect(pad_id, x, y, w, h, clip)
-
- yield d
-
- def __repr__(self):
- return f'THTPads(size={self.pad_size}, h={self.drill}, p={self.pitch}, sides={self.sides}, plated={self.plated}, pad_shape="{self.pad_shape}")'
-
- def symmetric_sides(self):
- return True
-
-
-class SMDProtoAreaRectangles(PatternProtoArea):
- def __init__(self, pitch_x, pitch_y, w=None, h=None, border=None):
- super().__init__(pitch_x, pitch_y, border=border)
- w = w or pitch_x - 0.15
- h = h or pitch_y - 0.15
- self.w, self.h = w, h
- self.pad_pattern = RectPattern(w, h, pitch_x, pitch_y)
- self.patterns = [self.pad_pattern]
-
- def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
- x, y, w, h = self.fit_rect(x, y, w, h, center)
- pad_id = str(uuid.uuid4())
- yield {'defs': [self.pad_pattern.svg_def(pad_id, x, y)],
- 'top copper': make_rect(pad_id, x, y, w, h, clip),
- 'top mask': make_rect(pad_id, x, y, w, h, clip)}
-
- def symmetric_sides(self):
- return False
-
-class ManhattanProtoArea(PatternProtoArea):
- def __init__(self, pitch=2.54*4, gap=0.25, border=None):
- super().__init__(pitch, pitch, border=border)
- self.gap = gap
- self.pad_pattern = ManhattanPattern(pitch, gap)
- self.patterns = [self.pad_pattern]
-
- def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
- x, y, w, h = self.fit_rect(x, y, w, h, center)
- pad_id = str(uuid.uuid4())
- yield {'defs': [self.pad_pattern.svg_def(pad_id, x, y)],
- 'top copper': make_rect(pad_id, x, y, w, h, clip),
- 'top mask': make_rect(pad_id, x, y, w, h, clip)}
-
- def symmetric_sides(self):
- return False
-
-LAYERS = [
- 'top paste',
- 'top silk',
- 'top mask',
- 'top copper',
- 'bottom copper',
- 'bottom mask',
- 'bottom silk',
- 'bottom paste',
- 'outline',
- 'nonplated drill',
- 'plated drill'
- ]
-
-class ProtoBoard:
- def __init__(self, defs, expr, mounting_holes=None, border=None, center=True, tight_layout=False):
- self.defs = eval_defs(defs)
- self.layout = parse_layout(expr, self.defs)
- self.mounting_holes = mounting_holes
- self.center = center
- self.tight_layout = tight_layout
-
- if border is None:
- self.border = (0, 0, 0, 0)
- elif hasattr(border, '__iter__'):
- if len(border == 4):
- self.border = border
- else:
- raise TypeError('border must be None, int, or a 4-tuple of floats (top, right, bottom, left)')
- else:
- self.border = (border, border, border, border)
-
- @property
- def symmetric_sides(self):
- return self.layout.symmetric_sides()
-
- @property
- def used_patterns(self):
- return set(self.layout.used_patterns())
-
- def generate(self, w, h):
- out = {l: [] for l in LAYERS}
- svg_defs = []
- clip = ''
-
- if self.mounting_holes:
- d, o, *k = self.mounting_holes # diameter, offset from edge, keepout to proto area
- k = k[0] if k else o
- q = o + k
- if 2*q < w:
- if 2*q < h:
- clip_d = f'M 0 {q} L {q} {q} L {q} 0 L {w-q} 0 L {w-q} {q} L {w} {q} L {w} {h-q} L {w-q} {h-q} L {w-q} {h} L {q} {h} L {q} {h-q} L 0 {h-q} Z'
- else:
- clip_d = f'M {q} 0 L {w-q} 0 L {w-q} {h} L 0 {h} Z'
- else:
- if 2*q < h:
- clip_d = f'M 0 {q} L 0 {h-q} L {w} {h-q} L {w} {q} Z'
- else:
- raise ValueError(f'Hole keepout areas are so large that no board area is left. Available size is {w}x{h} mm, keepout areas are {q}x{q} mm in all four corners.')
-
- svg_defs.append(f'')
- clip = 'clip-path="url(#hole-clip)"'
-
- out['nonplated drill'].append([
- f'',
- f'',
- f'',
- f'' ])
-
- t, r, b, l = self.border
- for layer_dict in self.layout.generate(l, t, w-l-r, h-t-b, self.center, clip, self.tight_layout):
- for l in LAYERS:
- if l in layer_dict:
- out[l].append(layer_dict[l])
- svg_defs += layer_dict.get('defs', [])
-
- out['outline'] = f''
-
- layers = [ make_layer(l, out[l]) for l in LAYERS ]
- return svg_template.format(w=w, h=h, defs='\n'.join(svg_defs), layers='\n'.join(layers))
-
-
-def convert_to_mm(value, unit):
- unitl = unit.lower()
- if unitl == 'mm':
- return value
- elif unitl == 'cm':
- return value*10
- elif unitl == 'in':
- return value*25.4
- elif unitl == 'mil':
- return value/1000*25.4
- else:
- raise ValueError(f'Invalid unit {unit}, allowed units are mm, cm, in, and mil.')
-
-value_re = re.compile('([0-9]*\.?[0-9]+)(cm|mm|in|mil|%)')
-def eval_value(value, total_length=None):
- if not isinstance(value, str):
- return None
-
- m = value_re.match(value.lower())
- number, unit = m.groups()
- if unit == '%':
- if total_length is None:
- raise ValueError('Percentages are not allowed for this value')
- return total_length * float(number) / 100
- return convert_to_mm(float(number), unit)
-
-class PropLayout:
- def __init__(self, content, direction, proportions):
- self.content = content
- self.direction = direction
- self.proportions = proportions
- if len(content) != len(proportions):
- raise ValueError('proportions and content must have same length')
-
- def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
- for (c_x, c_y, c_w, c_h), child in self.layout_2d(x, y, w, h, tight_layout):
- yield from child.generate(c_x, c_y, c_w, c_h, center, clip, tight_layout)
-
- def fit_size(self, w, h):
- widths = []
- heights = []
- for (_x, _y, w, h), child in self.layout_2d(0, 0, w, h, True):
- if not isinstance(child, EmptyProtoArea):
- widths.append(w)
- heights.append(h)
- if self.direction == 'h':
- return sum(widths), max(heights)
- else:
- return max(widths), sum(heights)
-
- def layout_2d(self, x, y, w, h, tight_layout=False):
- actual_l = 0
- target_l = 0
- for l, child in zip(self.layout(w if self.direction == 'h' else h), self.content):
- this_x, this_y = x, y
- this_w, this_h = w, h
- target_l += l
-
- if self.direction == 'h':
- this_w = target_l - actual_l
- else:
- this_h = target_l - actual_l
-
- if tight_layout:
- this_w, this_h = child.fit_size(this_w, this_h)
-
- if self.direction == 'h':
- x += this_w
- actual_l += this_w
- this_h = h
- else:
- y += this_h
- actual_l += this_h
- this_w = w
-
- yield (this_x, this_y, this_w, this_h), child
-
- def layout(self, length):
- out = [ eval_value(value, length) for value in self.proportions ]
- total_length = sum(value for value in out if value is not None)
- if length - total_length < -1e-6:
- raise ValueError(f'Proportions sum to {total_length} mm, which is greater than the available space of {length} mm.')
-
- leftover = length - total_length
- sum_props = sum( (value or 1.0) for value in self.proportions if not isinstance(value, str) )
- return [ (leftover * (value or 1.0) / sum_props if not isinstance(value, str) else calculated)
- for value, calculated in zip(self.proportions, out) ]
-
- def __str__(self):
- children = ', '.join( f'{elem}:{width}' for elem, width in zip(self.content, self.proportions))
- return f'PropLayout[{self.direction.upper()}]({children})'
-
- def symmetric_sides(self):
- return all(child.symmetric_sides() for child in self.content)
-
- def used_patterns(self):
- for child in self.content:
- yield from child.used_patterns()
-
-
-class TwoSideLayout:
- def __init__(self, top, bottom):
- self.top, self.bottom = top, bottom
-
- def flip(self, defs):
- out = dict(defs)
- for layer in ('copper', 'mask', 'silk', 'paste'):
- top, bottom = f'top {layer}', f'bottom {layer}'
- tval, bval = defs.get(top), defs.get(bottom)
-
- if tval:
- defs[bottom] = tval
- elif bottom in defs:
- del defs[bottom]
-
- if bval:
- defs[top] = bval
- elif top in defs:
- del defs[top]
-
- return defs
-
- def fit_size(self, w, h):
- top, bottom = self.top, self.bottom
- w1, h1 = top.fit_size(w, h)
- w2, h2 = bottom.fit_size(w, h)
- if isinstance(top, EmptyProtoArea):
- if isinstance(bottom, EmptyProtoArea):
- return w1, h1
- return w2, h2
- if isinstance(bottom, EmptyProtoArea):
- return w1, h1
- return max(w1, w2), max(h1, h2)
-
- def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
- yield from self.top.generate(x, y, w, h, center, clip, tight_layout)
- yield from map(self.flip, self.bottom.generate(x, y, w, h, center, clip, tight_layout))
-
- def symmetric_sides(self):
- return self.top == self.bottom
-
- def used_patterns(self):
- yield from self.top.used_patterns()
- yield from self.bottom.used_patterns()
-
-
-def _map_expression(node, defs):
- if isinstance(node, ast.Name):
- return defs[node.id]
-
- elif isinstance(node, ast.Constant):
- return node.value
-
-
- elif isinstance(node, ast.BinOp) and isinstance(node.op, (ast.BitOr, ast.BitAnd, ast.Add)):
- left_prop = right_prop = None
-
- left, right = node.left, node.right
-
- if isinstance(left, ast.BinOp) and isinstance(left.op, ast.MatMult):
- left_prop = _map_expression(left.right, defs)
- left = left.left
-
- if isinstance(right, ast.BinOp) and isinstance(right.op, ast.MatMult):
- right_prop = _map_expression(right.right, defs)
- right = right.left
-
- left, right = _map_expression(left, defs), _map_expression(right, defs)
-
- direction = 'h' if isinstance(node.op, ast.BitOr) else 'v'
- if isinstance(left, PropLayout) and left.direction == direction and left_prop is None:
- left.content.append(right)
- left.proportions.append(right_prop)
- return left
-
- elif isinstance(right, PropLayout) and right.direction == direction and right_prop is None:
- right.content.insert(0, left)
- right.proportions.insert(0, left_prop)
- return right
-
- elif isinstance(node.op, ast.Add):
- if left_prop or right_prop:
- raise SyntaxError(f'Proportions ("@") not supported for two-side layout ("+")')
-
- return TwoSideLayout(left, right)
-
- else:
- return PropLayout([left, right], direction, [left_prop, right_prop])
-
- elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult):
- raise SyntaxError(f'Unexpected width specification "{ast.unparse(node.right)}"')
-
- else:
- raise SyntaxError(f'Invalid layout expression "{ast.unparse(node)}"')
-
-def parse_layout(expr, defs):
- ''' Example layout:
-
- ( tht @ 2in | smd ) @ 50% / tht
- '''
-
- expr = re.sub(r'\s', '', expr)
- expr = re.sub(r'([0-9]*\.?[0-9]+)([Mm][Mm]|[Cc][Mm]|[Ii][Nn]|[Mm][Ii][Ll]|%)', r'"\1\2"', expr)
- expr = expr.replace('/', '&')
- try:
- expr = ast.parse(expr, mode='eval').body
- match expr:
- case ast.Name():
- return PropLayout([defs[expr.id]], 'h', [None])
-
- case ast.BinOp(op=ast.MatMult()):
- assert isinstance(expr.right, ast.Constant)
- return PropLayout([_map_expression(expr.left, defs)], 'h', [expr.right.value])
-
- case _:
- return _map_expression(expr, defs)
- except SyntaxError as e:
- raise SyntaxError('Invalid layout expression') from e
-
-PROTO_AREA_TYPES = {
- 'THTPads': THTProtoArea,
- 'SMDPads': SMDProtoAreaRectangles,
- 'Manhattan': ManhattanProtoArea,
- 'Empty': EmptyProtoArea,
-}
-
-def eval_defs(defs):
- defs = defs.replace('\n', ';')
- defs = re.sub(r'\s', '', defs)
-
- out = {}
- for elem in defs.split(';'):
- if not elem:
- continue
-
- if not (m := re.match('([a-zA-Z_][a-zA-Z0-9_]*)=([a-zA-Z_][a-zA-Z0-9_]*)\((.*)\)', elem)):
- raise SyntaxError(f'Invalid pattern definition "{elem}"')
-
- key, pattern, params = m.groups()
- args, kws = [], {}
- for elem in params.split(','):
- if not elem:
- continue
- if (m := re.match('([a-zA-Z_][a-zA-Z0-9_]*)=(.*)', elem)):
- param_name, param_value = m.groups()
- kws[param_name] = ast.literal_eval(param_value)
-
- else:
- args.append(ast.literal_eval(elem))
-
- out[key] = PROTO_AREA_TYPES[pattern](*args, **kws)
- return out
-
-COMMON_DEFS = '''
-empty = Empty(copper=False);
-ground = Empty(copper=True);
-
-tht = THTPads();
-manhattan = Manhattan();
-tht50 = THTPads(pad_size=1.0, drill=0.6, pitch=1.27);
-
-smd100 = SMDPads(1.27, 2.54);
-smd100r = SMDPads(2.54, 1.27);
-smd950 = SMDPads(0.95, 2.5);
-smd950r = SMDPads(2.5, 0.95);
-smd800 = SMDPads(0.80, 2.0);
-smd800r = SMDPads(2.0, 0.80);
-smd650 = SMDPads(0.65, 2.0);
-smd650r = SMDPads(2.0, 0.65);
-smd500 = SMDPads(0.5, 2.0);
-smd500r = SMDPads(2.0, 0.5);
-'''
-
-
-if __name__ == '__main__':
-# import sys
-# print('===== Layout expressions =====')
-# for line in [
-# 'tht',
-# 'tht@1mm',
-# 'tht|tht',
-# 'tht@1mm|tht',
-# 'tht|tht|tht',
-# 'tht@1mm|tht@2mm|tht@3mm',
-# '(tht@1mm|tht@2mm)|tht@3mm',
-# 'tht@1mm|(tht@2mm|tht@3mm)',
-# 'tht@2|tht|tht',
-# '(tht@1mm|tht|tht@3mm) / tht',
-# ]:
-# layout = parse_layout(line)
-# print(line, '->', layout)
-# print(' ', layout.layout(100))
-# print()
-# print('===== Pattern definitions =====')
-# for line in [
-# 'tht = THTCircles()',
-# 'tht = THTCircles(10)',
-# 'tht = THTCircles(10, 20)',
-# 'tht = THTCircles(plated=False)',
-# 'tht = THTCircles(10, plated=False)',
-# ]:
-# print(line, '->', eval_defs(line))
-# print()
-# print('===== Proto board =====')
- #b = ProtoBoard('tht = THTCircles(); tht_small = THTCircles(pad_size=1.0, drill=0.6, pitch=1.27)',
- # 'tht@1in|(tht_small@2/tht@1)', mounting_holes=(3.2, 5.0, 5.0), border=2, center=False)
- #b = ProtoBoard('tht = THTCircles(); smd1 = SMDPads(2.0, 2.0); smd2 = SMDPads(0.95, 1.895); plane=Empty(copper=True)', 'tht@25mm | (smd1 + plane)', mounting_holes=(3.2, 5.0, 5.0), border=2, tight_layout=True)
- #b = ProtoBoard(COMMON_DEFS, f'((smd100 + smd100) | (smd950 + smd950) | tht50@20mm)@20mm / tht', mounting_holes=(3.2,5,5), border=1, tight_layout=True, center=True)
- b = ProtoBoard(COMMON_DEFS, f'manhattan', mounting_holes=(3.2,5,5), border=1, tight_layout=True, center=True)
- print(b.generate(80, 60))
diff --git a/setup.py b/setup.py
deleted file mode 100755
index e548f2e..0000000
--- a/setup.py
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env python3
-
-import os
-import sys
-from setuptools import setup
-from setuptools.command.install import install
-import subprocess
-from multiprocessing import cpu_count
-from pathlib import Path
-import re
-
-def get_tag():
- try:
- res = subprocess.run(['git', '--git-dir', str(Path(__file__).with_name('.git')), 'describe', '--tags', '--match', 'v*'], capture_output=True, check=True, text=True)
- return res.stdout.strip()
- except subprocess.CalledProcessError as e:
- return 'v0.0.0-dev'
-
-def get_version():
- version, _, _rest = get_tag()[1:].partition('-')
- return version
-
-def format_readme_for_pypi():
- tag = get_tag()
- # Replace repo-relative image URLs with gitlab raw URLs. Gitlab and github render repo-relative URLs just fine, but
- # PyPI doesn't.
- return '\n'.join(
- re.sub('^.. (figure|image):: (pics/.*)$', f'.. \\1:: https://gitlab.com/gerbolyze/gerbolyze/-/raw/{tag}/\\2', line.strip('\n'))
- for line in Path('README.rst').read_text().splitlines())
-
-setup(
- name = 'gerbolyze',
- version = get_version(),
- packages=['gerbolyze'],
- scripts=['bin/gerbolyze'],
- description = ('A high-resolution image-to-PCB converter. Gerbolyze plots SVG, PNG and JPG onto existing gerber '
- 'files. It handles almost the full SVG spec and deals with text, path outlines, patterns, arbitrary paths with '
- 'self-intersections and holes, etc. fully automatically. It can vectorize raster images both by contour '
- 'tracing and by grayscale dithering. All processing is done at the vector level without intermediate '
- 'conversions to raster images accurately preserving the input.'),
- long_description=format_readme_for_pypi(),
- long_description_content_type='text/x-rst',
- url='https://github.com/jaseg/gerbolyze',
- project_urls={
- 'Source Code': 'https://git.jaseg.de/gerbolyze',
- 'Bug Tracker': 'https://github.com/jaseg/gerbolyze/issues',
- },
- author = 'jaseg',
- author_email = 'gerbonara@jaseg.de',
- install_requires = ['gerbonara >= 1.2.0', 'beautifulsoup4', 'numpy', 'python-slugify', 'lxml', 'click', 'svg-flatten-wasi'],
- license = 'AGPLv3',
- classifiers = [
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Console',
- 'Intended Audience :: Manufacturing',
- 'Intended Audience :: Science/Research',
- 'Intended Audience :: Religion',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
- 'Natural Language :: English',
- 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
- 'Topic :: Utilities'
- ]
-)
-
diff --git a/gerbolyze/__init__.py b/src/gerbolyze/__init__.py
similarity index 100%
rename from gerbolyze/__init__.py
rename to src/gerbolyze/__init__.py
diff --git a/gerbolyze/__main__.py b/src/gerbolyze/__main__.py
similarity index 100%
rename from gerbolyze/__main__.py
rename to src/gerbolyze/__main__.py
diff --git a/gerbolyze/tests/__init__.py b/tests/__init__.py
similarity index 100%
rename from gerbolyze/tests/__init__.py
rename to tests/__init__.py
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-B.Cu.gbr b/tests/resources/layers-gerber/layers-B.Cu.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-B.Cu.gbr
rename to tests/resources/layers-gerber/layers-B.Cu.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-B.Mask.gbr b/tests/resources/layers-gerber/layers-B.Mask.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-B.Mask.gbr
rename to tests/resources/layers-gerber/layers-B.Mask.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-B.Paste.gbr b/tests/resources/layers-gerber/layers-B.Paste.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-B.Paste.gbr
rename to tests/resources/layers-gerber/layers-B.Paste.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-B.SilkS.gbr b/tests/resources/layers-gerber/layers-B.SilkS.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-B.SilkS.gbr
rename to tests/resources/layers-gerber/layers-B.SilkS.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-Cmts.User.gbr b/tests/resources/layers-gerber/layers-Cmts.User.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-Cmts.User.gbr
rename to tests/resources/layers-gerber/layers-Cmts.User.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-Edge.Cuts.gbr b/tests/resources/layers-gerber/layers-Edge.Cuts.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-Edge.Cuts.gbr
rename to tests/resources/layers-gerber/layers-Edge.Cuts.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-F.Cu.gbr b/tests/resources/layers-gerber/layers-F.Cu.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-F.Cu.gbr
rename to tests/resources/layers-gerber/layers-F.Cu.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-F.Mask.gbr b/tests/resources/layers-gerber/layers-F.Mask.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-F.Mask.gbr
rename to tests/resources/layers-gerber/layers-F.Mask.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-F.Paste.gbr b/tests/resources/layers-gerber/layers-F.Paste.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-F.Paste.gbr
rename to tests/resources/layers-gerber/layers-F.Paste.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-F.SilkS.gbr b/tests/resources/layers-gerber/layers-F.SilkS.gbr
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-F.SilkS.gbr
rename to tests/resources/layers-gerber/layers-F.SilkS.gbr
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-NPTH.drl b/tests/resources/layers-gerber/layers-NPTH.drl
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-NPTH.drl
rename to tests/resources/layers-gerber/layers-NPTH.drl
diff --git a/gerbolyze/tests/resources/layers-gerber/layers-PTH.drl b/tests/resources/layers-gerber/layers-PTH.drl
similarity index 100%
rename from gerbolyze/tests/resources/layers-gerber/layers-PTH.drl
rename to tests/resources/layers-gerber/layers-PTH.drl
diff --git a/gerbolyze/tests/resources/layers.svg b/tests/resources/layers.svg
similarity index 100%
rename from gerbolyze/tests/resources/layers.svg
rename to tests/resources/layers.svg
diff --git a/gerbolyze/tests/resources/svg_feature_test.svg b/tests/resources/svg_feature_test.svg
similarity index 100%
rename from gerbolyze/tests/resources/svg_feature_test.svg
rename to tests/resources/svg_feature_test.svg
diff --git a/gerbolyze/tests/resources/test_gerber_8seg.zip b/tests/resources/test_gerber_8seg.zip
similarity index 100%
rename from gerbolyze/tests/resources/test_gerber_8seg.zip
rename to tests/resources/test_gerber_8seg.zip
diff --git a/gerbolyze/tests/resources/tpl-bottom.svg b/tests/resources/tpl-bottom.svg
similarity index 100%
rename from gerbolyze/tests/resources/tpl-bottom.svg
rename to tests/resources/tpl-bottom.svg
diff --git a/gerbolyze/tests/resources/tpl-top.svg b/tests/resources/tpl-top.svg
similarity index 100%
rename from gerbolyze/tests/resources/tpl-top.svg
rename to tests/resources/tpl-top.svg
diff --git a/gerbolyze/tests/test_integration.py b/tests/test_integration.py
similarity index 100%
rename from gerbolyze/tests/test_integration.py
rename to tests/test_integration.py
diff --git a/gerbolyze/tests/test_regression.py b/tests/test_regression.py
similarity index 100%
rename from gerbolyze/tests/test_regression.py
rename to tests/test_regression.py