From ee4ad9d6022bb5f930ce0ec51947fa146ccff106 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 19 Jun 2022 18:42:02 +0200 Subject: protoboard: initial commit --- gerbolyze/protoboard.py | 239 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 gerbolyze/protoboard.py diff --git a/gerbolyze/protoboard.py b/gerbolyze/protoboard.py new file mode 100644 index 0000000..2f45980 --- /dev/null +++ b/gerbolyze/protoboard.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 + +import re +import textwrap +import ast + +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, content): + self.w = w + self.h = h + self.content = content + + @property + def svg_id(self): + return f'pat-{id(self):16x}' + + def __str__(self): + return textwrap.dedent(f''' + + {svg_str(self.content)} + ''') + + def make_rect(x, y, w, h): + return f'' + +class CirclePattern(Pattern): + def __init__(self, d, w, h=None): + self.d = d + self.w = w + self.h = h or w + + @property + def content(self): + return f'' + +make_layer = lambda layer_name, content: \ + f'{svg_str(content)}' + +svg_template = textwrap.dedent(''' + + + + {defs} + + + {layers} + +''') + +class PatternProtoArea: + def __init__(self, pitch_x, pitch_y=None): + self.pitch_x = pitch_x + self.pitch_y = pitch_y or pitch_x + + @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_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) + + 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 + +class THTProtoAreaCircles: + def __init__(self, pad_dia=2.0, drill=1.0, pitch=2.54, sides='both', plated=True): + super(pitch) + self.pad_dia = pad_dia + self.drill = drill + self.drill_pattern = CirclePattern(self.drill, self.pitch) + self.pad_pattern = CirclePattern(self.pad_dia, 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): + 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) } + + if self.sides in ('top', 'both'): + d['top copper'] = self.pad_pattern.make_rect(x, y, w, h) + if self.sides in ('bottom', 'both'): + d['bottom copper'] = self.pad_pattern.make_rect(x, y, w, h) + + return d + +class ProtoBoard: + def __init__(self, desc_str): + pass + +def convert_to_mm(value, unit): + match unit.lower(): + case 'mm': return value + case 'cm': return value*10 + case 'in': return value*25.4 + case 'mil': return value/1000*25.4 + 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) + 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 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 _map_expression(node): + match node: + case ast.Name(): + return node.id + + case ast.Constant(): + return node.value + + case ast.BinOp(op=ast.BitOr()) | ast.BinOp(op=ast.BitAnd()): + 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) + left = left.left + + if isinstance(right, ast.BinOp) and isinstance(right.op, ast.MatMult): + right_prop = _map_expression(right.right) + right = right.left + + direction = 'h' if isinstance(node.op, ast.BitOr) else 'v' + left, right = _map_expression(left), _map_expression(right) + + 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 + + else: + return PropLayout([left, right], direction, [left_prop, right_prop]) + + case ast.BinOp(op=ast.MatMult()): + raise SyntaxError(f'Unexpected width specification "{ast.unparse(node.right)}"') + + case _: + raise SyntaxError(f'Invalid layout expression "{ast.unparse(node)}"') + +def parse_layout(expr): + ''' Example layout: + + ( 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 = expr.replace('/', '&') + try: + expr = ast.parse(expr, mode='eval').body + match expr: + case ast.Name(): + return PropLayout([expr.id], 'h', [None]) + + case ast.BinOp(op=ast.MatMult()): + assert isinstance(expr.right, ast.Constant) + return PropLayout([_map_expression(expr.left)], 'h', [expr.right.value]) + + case _: + return _map_expression(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() + -- cgit