summaryrefslogtreecommitdiff
path: root/gerbonara/aperture_macros/primitive.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/aperture_macros/primitive.py')
-rw-r--r--gerbonara/aperture_macros/primitive.py270
1 files changed, 270 insertions, 0 deletions
diff --git a/gerbonara/aperture_macros/primitive.py b/gerbonara/aperture_macros/primitive.py
new file mode 100644
index 0000000..8732520
--- /dev/null
+++ b/gerbonara/aperture_macros/primitive.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+# Copyright 2022 Jan Götte <gerbonara@jaseg.de>
+
+import warnings
+import contextlib
+import math
+
+from .expression import Expression, UnitExpression, ConstantExpression, expr
+
+from .. import graphic_primitives as gp
+
+
+def point_distance(a, b):
+ x1, y1 = a
+ x2, y2 = b
+ return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
+
+def deg_to_rad(a):
+ return (a / 180) * math.pi
+
+class Primitive:
+ def __init__(self, unit, args):
+ self.unit = unit
+
+ if len(args) > len(type(self).__annotations__):
+ raise ValueError(f'Too many arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})')
+
+ for arg, (name, fieldtype) in zip(args, type(self).__annotations__.items()):
+ arg = expr(arg) # convert int/float to Expression object
+
+ if fieldtype == UnitExpression:
+ setattr(self, name, UnitExpression(arg, unit))
+ else:
+ setattr(self, name, arg)
+
+ for name in type(self).__annotations__:
+ if not hasattr(self, name):
+ raise ValueError(f'Too few arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})')
+
+ def to_gerber(self, unit=None):
+ return f'{self.code},' + ','.join(
+ getattr(self, name).to_gerber(unit) for name in type(self).__annotations__)
+
+ def __str__(self):
+ attrs = ','.join(str(getattr(self, name)).strip('<>') for name in type(self).__annotations__)
+ return f'<{type(self).__name__} {attrs}>'
+
+ def __repr__(self):
+ return str(self)
+
+ class Calculator:
+ def __init__(self, instance, variable_binding={}, unit=None):
+ self.instance = instance
+ self.variable_binding = variable_binding
+ self.unit = unit
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, _type, _value, _traceback):
+ pass
+
+ def __getattr__(self, name):
+ return getattr(self.instance, name).calculate(self.variable_binding, self.unit)
+
+ def __call__(self, expr):
+ return expr.calculate(self.variable_binding, self.unit)
+
+
+class Circle(Primitive):
+ code = 1
+ exposure : Expression
+ diameter : UnitExpression
+ # center x/y
+ x : UnitExpression
+ y : UnitExpression
+ rotation : Expression = None
+
+ def __init__(self, unit, args):
+ super().__init__(unit, args)
+ if self.rotation is None:
+ self.rotation = ConstantExpression(0)
+
+ def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
+ with self.Calculator(self, variable_binding, unit) as calc:
+ x, y = gp.rotate_point(calc.x, calc.y, deg_to_rad(calc.rotation) + rotation, 0, 0)
+ x, y = x+offset[0], y+offset[1]
+ return [ gp.Circle(x, y, calc.diameter/2, polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
+
+ def dilate(self, offset, unit):
+ self.diameter += UnitExpression(offset, unit)
+
+class VectorLine(Primitive):
+ code = 20
+ exposure : Expression
+ width : UnitExpression
+ start_x : UnitExpression
+ start_y : UnitExpression
+ end_x : UnitExpression
+ end_y : UnitExpression
+ rotation : Expression
+
+ def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
+ with self.Calculator(self, variable_binding, unit) as calc:
+ center_x = (calc.end_x + calc.start_x) / 2
+ center_y = (calc.end_y + calc.start_y) / 2
+ delta_x = calc.end_x - calc.start_x
+ delta_y = calc.end_y - calc.start_y
+ length = point_distance((calc.start_x, calc.start_y), (calc.end_x, calc.end_y))
+
+ center_x, center_y = center_x+offset[0], center_y+offset[1]
+ rotation += deg_to_rad(calc.rotation) + math.atan2(delta_y, delta_x)
+
+ return [ gp.Rectangle(center_x, center_y, length, calc.width, rotation=rotation,
+ polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
+
+ def dilate(self, offset, unit):
+ self.width += UnitExpression(2*offset, unit)
+
+
+class CenterLine(Primitive):
+ code = 21
+ exposure : Expression
+ width : UnitExpression
+ height : UnitExpression
+ # center x/y
+ x : UnitExpression
+ y : UnitExpression
+ rotation : Expression
+
+ def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
+ with self.Calculator(self, variable_binding, unit) as calc:
+ rotation += deg_to_rad(calc.rotation)
+ x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
+ x, y = x+offset[0], y+offset[1]
+ w, h = calc.width, calc.height
+
+ return [ gp.Rectangle(x, y, w, h, rotation, polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
+
+ def dilate(self, offset, unit):
+ self.width += UnitExpression(2*offset, unit)
+
+
+class Polygon(Primitive):
+ code = 5
+ exposure : Expression
+ n_vertices : Expression
+ # center x/y
+ x : UnitExpression
+ y : UnitExpression
+ diameter : UnitExpression
+ rotation : Expression
+
+ def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
+ with self.Calculator(self, variable_binding, unit) as calc:
+ rotation += deg_to_rad(calc.rotation)
+ x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
+ x, y = x+offset[0], y+offset[1]
+ return [ gp.RegularPolygon(calc.x, calc.y, calc.diameter/2, calc.n_vertices, rotation,
+ polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
+
+ def dilate(self, offset, unit):
+ self.diameter += UnitExpression(2*offset, unit)
+
+
+class Thermal(Primitive):
+ code = 7
+ exposure : Expression
+ # center x/y
+ x : UnitExpression
+ y : UnitExpression
+ d_outer : UnitExpression
+ d_inner : UnitExpression
+ gap_w : UnitExpression
+ rotation : Expression
+
+ def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
+ with self.Calculator(self, variable_binding, unit) as calc:
+ rotation += deg_to_rad(calc.rotation)
+ x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
+ x, y = x+offset[0], y+offset[1]
+
+ dark = (bool(calc.exposure) == polarity_dark)
+
+ return [
+ gp.Circle(x, y, calc.d_outer/2, polarity_dark=dark),
+ gp.Circle(x, y, calc.d_inner/2, polarity_dark=not dark),
+ gp.Rectangle(x, y, d_outer, gap_w, rotation=rotation, polarity_dark=not dark),
+ gp.Rectangle(x, y, gap_w, d_outer, rotation=rotation, polarity_dark=not dark),
+ ]
+
+ def dilate(self, offset, unit):
+ # I'd rather print a warning and produce graphically slightly incorrect output in these few cases here than
+ # producing macros that may evaluate to primitives with negative values.
+ warnings.warn('Attempted dilation of macro aperture thermal primitive. This is not supported.')
+
+
+class Outline(Primitive):
+ code = 4
+
+ def __init__(self, unit, args):
+ if len(args) < 11:
+ raise ValueError(f'Invalid aperture macro outline primitive, not enough parameters ({len(args)}).')
+ if len(args) > 5004:
+ raise ValueError(f'Invalid aperture macro outline primitive, too many points ({len(args)//2-2}).')
+
+ self.exposure = args.pop(0)
+
+ # length arg must not contain variables (that would not make sense)
+ length_arg = args.pop(0).calculate()
+
+ if length_arg != len(args)//2-1:
+ raise ValueError(f'Invalid aperture macro outline primitive, given size {length_arg} does not match length of coordinate list({len(args)//2-1}).')
+
+ if len(args) % 2 == 1:
+ self.rotation = args.pop()
+ else:
+ self.rotation = ConstantExpression(0.0)
+
+ if args[0] != args[-2] or args[1] != args[-1]:
+ raise ValueError(f'Invalid aperture macro outline primitive, polygon is not closed {args[2:4], args[-3:-1]}')
+
+ self.coords = [(UnitExpression(x, unit), UnitExpression(y, unit)) for x, y in zip(args[0::2], args[1::2])]
+
+ def __str__(self):
+ return f'<Outline {len(self.coords)} points>'
+
+ def to_gerber(self, unit=None):
+ coords = ','.join(coord.to_gerber(unit) for xy in self.coords for coord in xy)
+ return f'{self.code},{self.exposure.to_gerber()},{len(self.coords)-1},{coords},{self.rotation.to_gerber()}'
+
+ def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
+ with self.Calculator(self, variable_binding, unit) as calc:
+ rotation += deg_to_rad(calc.rotation)
+ bound_coords = [ gp.rotate_point(calc(x), calc(y), rotation, 0, 0) for x, y in self.coords ]
+ bound_coords = [ (x+offset[0], y+offset[1]) for x, y in bound_coords ]
+ bound_radii = [None] * len(bound_coords)
+ return [gp.ArcPoly(bound_coords, bound_radii, polarity_dark=(bool(calc.exposure) == polarity_dark))]
+
+ def dilate(self, offset, unit):
+ # we would need a whole polygon offset/clipping library here
+ warnings.warn('Attempted dilation of macro aperture outline primitive. This is not supported.')
+
+
+class Comment:
+ code = 0
+
+ def __init__(self, comment):
+ self.comment = comment
+
+ def to_gerber(self, unit=None):
+ return f'0 {self.comment}'
+
+PRIMITIVE_CLASSES = {
+ **{cls.code: cls for cls in [
+ Comment,
+ Circle,
+ VectorLine,
+ CenterLine,
+ Outline,
+ Polygon,
+ Thermal,
+ ]},
+ # alternative codes
+ 2: VectorLine,
+}
+