aboutsummaryrefslogtreecommitdiff
path: root/gerbolyze/protoboard.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbolyze/protoboard.py')
-rw-r--r--gerbolyze/protoboard.py184
1 files changed, 145 insertions, 39 deletions
diff --git a/gerbolyze/protoboard.py b/gerbolyze/protoboard.py
index 2f45980..b2e9d95 100644
--- a/gerbolyze/protoboard.py
+++ b/gerbolyze/protoboard.py
@@ -3,6 +3,7 @@
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)
@@ -12,18 +13,17 @@ class Pattern:
self.h = h
self.content = content
- @property
- def svg_id(self):
- return f'pat-{id(self):16x}'
-
- def __str__(self):
+ def svg_def(self, svg_id, off_x, off_y):
return textwrap.dedent(f'''
- <pattern id="{self.svg_id}" viewBox="0,0,{self.w},{self.h}" width="{self.w}" height="{self.h}" patternUnits="userSpaceOnUse">
+ <pattern id="{svg_id}" x="{off_x}" y="{off_y}" viewBox="0,0,{self.w},{self.h}" width="{self.w}" height="{self.h}" patternUnits="userSpaceOnUse">
{svg_str(self.content)}
</pattern>''')
- def make_rect(x, y, w, h):
- return f'<rect x="{x}" y="{y}" w="{w}" h="{h}" fill="url(#{self.svg_id})"/>'
+def make_rect(svg_id, x, y, w, h):
+ #import random
+ #c = random.randint(0, 2**24)
+ #return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="#{c:06x}"/>'
+ return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="url(#{svg_id})"/>'
class CirclePattern(Pattern):
def __init__(self, d, w, h=None):
@@ -33,7 +33,7 @@ class CirclePattern(Pattern):
@property
def content(self):
- return f'<circle cx={self.w/2} cy={self.h/2} r={self.d/2}/>'
+ return f'<circle cx="{self.w/2}" cy="{self.h/2}" r="{self.d/2}"/>'
make_layer = lambda layer_name, content: \
f'<g id="g-{layer_name.replace(" ", "-")}" inkscape:label="{layer_name}" inkscape:groupmode="layer">{svg_str(content)}</g>'
@@ -56,7 +56,7 @@ svg_template = textwrap.dedent('''
inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" />
{layers}
</svg>
-''')
+''').strip()
class PatternProtoArea:
def __init__(self, pitch_x, pitch_y=None):
@@ -70,7 +70,8 @@ class PatternProtoArea:
return self.pitch_x
def fit_rect(self, x, y, w, h, center=True):
- w_fit, h_fit = round(w - (w % self.pitch_x), 6), round(h - (h % self.pitch_y), 6)
+ 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
@@ -80,9 +81,9 @@ class PatternProtoArea:
else:
return x, y, w_fit, h_fit
-class THTProtoAreaCircles:
+class THTProtoAreaCircles(PatternProtoArea):
def __init__(self, pad_dia=2.0, drill=1.0, pitch=2.54, sides='both', plated=True):
- super(pitch)
+ super().__init__(pitch)
self.pad_dia = pad_dia
self.drill = drill
self.drill_pattern = CirclePattern(self.drill, self.pitch)
@@ -94,18 +95,57 @@ class THTProtoAreaCircles:
def generate(self, x, y, w, h, center=True):
x, y, w, h = self.fit_rect(x, y, w, h, center)
drill = 'plated drill' if self.plated else 'nonplated drill'
- d = { drill: self.drill_pattern.make_rect(x, y, w, h) }
+
+ pad_id = str(uuid.uuid4())
+ drill_id = str(uuid.uuid4())
+
+ d = { drill: make_rect(drill_id, x, y, w, h),
+ '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'] = self.pad_pattern.make_rect(x, y, w, h)
+ d['top copper'] = make_rect(pad_id, x, y, w, h)
if self.sides in ('bottom', 'both'):
- d['bottom copper'] = self.pad_pattern.make_rect(x, y, w, h)
+ d['bottom copper'] = make_rect(pad_id, x, y, w, h)
return d
+ def __repr__(self):
+ return f'THTCircles(d={self.pad_dia}, h={self.drill}, p={self.pitch}, sides={self.sides}, plated={self.plated})'
+
+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, desc_str):
- pass
+ def __init__(self, defs, expr):
+ self.defs = eval_defs(defs)
+ self.layout = parse_layout(expr)
+
+ def generate(self, w, h):
+ svg_defs = []
+
+ out = {l: [] for l in LAYERS}
+ for layer_dict in self.layout.generate(0, 0, w, h, self.defs):
+ for l in LAYERS:
+ if l in layer_dict:
+ out[l].append(layer_dict[l])
+ svg_defs += layer_dict.get('defs', [])
+
+ 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):
match unit.lower():
@@ -120,7 +160,7 @@ def eval_value(value, total_length=None):
if not isinstance(value, str):
return None
- m = value_re.match(value)
+ m = value_re.match(value.lower())
number, unit = m.groups()
if unit == '%':
if total_length is None:
@@ -136,6 +176,28 @@ class PropLayout:
if len(content) != len(proportions):
raise ValueError('proportions and content must have same length')
+ def generate(self, x, y, w, h, defs):
+ for (c_x, c_y, c_w, c_h), child in self.layout_2d(x, y, w, h):
+ if isinstance(child, str):
+ yield defs[child].generate(c_x, c_y, c_w, c_h, defs)
+
+ else:
+ yield from child.generate(c_x, c_y, c_w, c_h, defs)
+
+ def layout_2d(self, x, y, w, h):
+ for l, child in zip(self.layout(w if self.direction == 'h' else h), self.content):
+ this_w, this_h = w, h
+ this_x, this_y = x, y
+
+ if self.direction == 'h':
+ this_w = l
+ x += l
+ else:
+ this_h = l
+ y += l
+
+ 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)
@@ -200,8 +262,8 @@ def parse_layout(expr):
( tht @ 2in | smd ) @ 50% / tht
'''
- expr = re.sub(r'\s', '', expr).lower()
- expr = re.sub(r'([0-9]*\.?[0-9]+)(mm|cm|in|mil|%)', r'"\1\2"', expr)
+ 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
@@ -218,22 +280,66 @@ def parse_layout(expr):
except SyntaxError as e:
raise SyntaxError('Invalid layout expression') from e
-if __name__ == '__main__':
- import sys
- 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()
+PROTO_AREA_TYPES = {
+ 'THTCircles': THTProtoAreaCircles
+}
+
+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
+
+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@1in|(tht@2/tht@1)')
+ print(b.generate(80, 60))