#!/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()