summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerbonara/gerber/aperture_macros/expression.py41
-rw-r--r--gerbonara/gerber/aperture_macros/parse.py14
-rw-r--r--gerbonara/gerber/aperture_macros/primitive.py29
-rw-r--r--gerbonara/gerber/apertures.py27
-rw-r--r--gerbonara/gerber/rs274x.py41
-rw-r--r--gerbonara/gerber/tests/image_support.py2
6 files changed, 150 insertions, 4 deletions
diff --git a/gerbonara/gerber/aperture_macros/expression.py b/gerbonara/gerber/aperture_macros/expression.py
index 390b7b7..fb399d3 100644
--- a/gerbonara/gerber/aperture_macros/expression.py
+++ b/gerbonara/gerber/aperture_macros/expression.py
@@ -90,6 +90,47 @@ class UnitExpression(Expression):
else:
raise ValueError(f'invalid unit {unit}, must be "inch" or "mm".')
+ def __add__(self, other):
+ if not isinstance(other, UnitExpression):
+ raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.')
+
+ if self.unit == other.unit or self.unit is None or other.unit is None:
+ return UnitExpression(self._expr + other._expr, self.unit)
+
+ if other.unit == 'mm': # -> and self.unit == 'inch'
+ return UnitExpression(self._expr + (other._expr / MILLIMETERS_PER_INCH), self.unit)
+ else: # other.unit == 'inch' and self.unit == 'mm'
+ return UnitExpression(self._expr + (other._expr * MILLIMETERS_PER_INCH), self.unit)
+
+ def __radd__(self, other):
+ # left hand side cannot have been an UnitExpression or __radd__ would not have been called
+ raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.')
+
+ def __sub__(self, other):
+ return (self + (-other)).optimize()
+
+ def __rsub__(self, other):
+ # see __radd__ above
+ raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.')
+
+ def __mul__(self, other):
+ return UnitExpression(self._expr * other, self.unit)
+
+ def __rmul__(self, other):
+ return UnitExpression(other * self._expr, self.unit)
+
+ def __truediv__(self, other):
+ return UnitExpression(self._expr / other, self.unit)
+
+ def __rtruediv__(self, other):
+ return UnitExpression(other / self._expr, self.unit)
+
+ def __neg__(self):
+ return UnitExpression(-self._expr, self.unit)
+
+ def __pos__(self):
+ return self
+
class ConstantExpression(Expression):
def __init__(self, value):
diff --git a/gerbonara/gerber/aperture_macros/parse.py b/gerbonara/gerber/aperture_macros/parse.py
index 86f2882..00227c6 100644
--- a/gerbonara/gerber/aperture_macros/parse.py
+++ b/gerbonara/gerber/aperture_macros/parse.py
@@ -98,6 +98,20 @@ class ApertureMacro:
def __hash__(self):
return hash(self.to_gerber())
+ def dilated(self, offset, unit='mm'):
+ dup = copy.deepcopy(self)
+ new_primitives = []
+ for primitive in dup.primitives:
+ try:
+ if primitive.exposure.calculate():
+ primitive.dilate(offset, unit)
+ new_primitives.append(primitive)
+ except IndexError:
+ warnings.warn('Cannot dilate aperture macro primitive with exposure value computed from macro variable.')
+ pass
+ dup.primitives = new_primitives
+ return dup
+
def to_gerber(self, unit=None):
comments = [ c.to_gerber() for c in self.comments ]
variable_defs = [ f'${var.to_gerber(unit)}={expr}' for var, expr in self.variables.items() ]
diff --git a/gerbonara/gerber/aperture_macros/primitive.py b/gerbonara/gerber/aperture_macros/primitive.py
index a587d7e..b28fdb5 100644
--- a/gerbonara/gerber/aperture_macros/primitive.py
+++ b/gerbonara/gerber/aperture_macros/primitive.py
@@ -4,6 +4,7 @@
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
# Copyright 2022 Jan Götte <gerbonara@jaseg.de>
+import warnings
import contextlib
import math
@@ -20,6 +21,13 @@ def point_distance(a, b):
def deg_to_rad(a):
return (a / 180) * math.pi
+def convert(value, src, dst):
+ if src == dst or src is None or dst is None or value is None:
+ return value
+ elif dst == 'mm':
+ return value * 25.4
+ else:
+ return value / 25.4
class Primitive:
def __init__(self, unit, args):
@@ -88,6 +96,9 @@ class Circle(Primitive):
x, y = x+offset[0], y+offset[1]
return [ gp.Circle(x, y, calc.r, polarity_dark=bool(calc.exposure)) ]
+ def dilate(self, offset, unit):
+ self.diameter += UnitExpression(offset, unit)
+
class VectorLine(Primitive):
code = 20
exposure : Expression
@@ -112,6 +123,9 @@ class VectorLine(Primitive):
return [ gp.Rectangle(center_x, center_y, length, calc.width, rotation=rotation,
polarity_dark=bool(calc.exposure)) ]
+ def dilate(self, offset, unit):
+ self.width += UnitExpression(2*offset, unit)
+
class CenterLine(Primitive):
code = 21
@@ -131,6 +145,9 @@ class CenterLine(Primitive):
w, h = calc.width, calc.height
return [ gp.Rectangle(x, y, w, h, rotation, polarity_dark=bool(calc.exposure)) ]
+
+ def dilate(self, offset, unit):
+ self.width += UnitExpression(2*offset, unit)
class Polygon(Primitive):
@@ -151,6 +168,9 @@ class Polygon(Primitive):
return [ gp.RegularPolygon(calc.x, calc.y, calc.diameter/2, calc.n_vertices, rotation,
polarity_dark=bool(calc.exposure)) ]
+ def dilate(self, offset, unit):
+ self.diameter += UnitExpression(2*offset, unit)
+
class Thermal(Primitive):
code = 7
@@ -178,6 +198,11 @@ class Thermal(Primitive):
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
@@ -220,6 +245,10 @@ class Outline(Primitive):
return gp.ArcPoly(bound_coords, bound_radii, polarity_dark=calc.exposure)
+ 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
diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py
index 2723c8d..7c98775 100644
--- a/gerbonara/gerber/apertures.py
+++ b/gerbonara/gerber/apertures.py
@@ -50,6 +50,14 @@ class Aperture:
else:
return value / 25.4
+ def convert_from(self, value, unit):
+ if self.unit == unit or self.unit is None or unit is None or value is None:
+ return value
+ elif unit == 'mm':
+ return value / 25.4
+ else:
+ return value * 25.4
+
def params(self, unit=None):
out = []
for f in fields(self):
@@ -112,6 +120,10 @@ class CircleAperture(Aperture):
def equivalent_width(self):
return self.diameter
+ def dilated(self, offset, unit='mm'):
+ offset = self.convert_from(offset, unit)
+ return replace(self, diameter=self.diameter+2*offset, hole_dia=None, hole_rect_h=None)
+
def _rotated(self):
if math.isclose(self.rotation % (2*math.pi), 0) or self.hole_rect_h is None:
return self
@@ -150,6 +162,10 @@ class RectangleAperture(Aperture):
def equivalent_width(self):
return math.sqrt(self.w**2 + self.h**2)
+ def dilated(self, offset, unit='mm'):
+ offset = self.convert_from(offset, unit)
+ return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
+
def _rotated(self):
if math.isclose(self.rotation % math.pi, 0):
return self
@@ -192,6 +208,10 @@ class ObroundAperture(Aperture):
flash = _flash_hole
+ def dilated(self, offset, unit='mm'):
+ offset = self.convert_from(offset, unit)
+ return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
+
def _rotated(self):
if math.isclose(self.rotation % math.pi, 0):
return self
@@ -232,6 +252,10 @@ class PolygonAperture(Aperture):
def __str__(self):
return f'<{self.n_vertices}-gon aperture d={self.diameter:.3}'
+ def dilated(self, offset, unit='mm'):
+ offset = self.convert_from(offset, unit)
+ return replace(self, diameter=self.diameter+2*offset, hole_dia=None)
+
flash = _flash_hole
def _rotated(self):
@@ -266,6 +290,9 @@ class ApertureMacroInstance(Aperture):
# FIXME return graphical primitives not macro primitives here
return [ primitive.with_offset(x, y).rotated(self.rotation, cx=0, cy=0) for primitive in self._primitives ]
+ def dilated(self, offset, unit='mm'):
+ return replace(self, macro=self.macro.dilated(offset, unit))
+
def _rotated(self):
if math.isclose(self.rotation % (2*math.pi), 0):
return self
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py
index 4313a11..38dfe06 100644
--- a/gerbonara/gerber/rs274x.py
+++ b/gerbonara/gerber/rs274x.py
@@ -49,6 +49,16 @@ def convert(self, value, src, dst):
else:
return value / 25.4
+def points_close(a, b):
+ if a == b:
+ return True
+ elif a is None or b is None:
+ return False
+ elif None in a or None in b:
+ return False
+ else:
+ return math.isclose(a[0], b[0]) and math.isclose(a[1], b[1])
+
class GerberFile(CamFile):
""" A class representing a single gerber file
@@ -102,6 +112,32 @@ class GerberFile(CamFile):
macro.name = new_name
seen_macro_names.add(new_name)
+ def dilate(self, offset, unit='mm', polarity_dark=True):
+
+ self.apertures = [ aperture.dilated(offset, unit) for aperture in self.apertures ]
+
+ offset_circle = CircleAperture(offset, unit=unit)
+ self.apertures.append(offset_circle)
+
+ new_primitives = []
+ for p in self.primitives:
+
+ p.polarity_dark = polarity_dark
+
+ # Ignore Line, Arc, Flash. Their actual dilation has already been done by dilating the apertures above.
+ if isinstance(p, Region):
+ ol = p.poly.outline
+ for start, end, arc_center in zip(ol, ol[1:] + ol[0], p.poly.arc_centers):
+ if arc_center is not None:
+ new_primitives.append(Arc(*start, *end, *arc_center,
+ polarity_dark=polarity_dark, unit=p.unit, aperture=offset_circle))
+
+ else:
+ new_primitives.append(Line(*start, *end,
+ polarity_dark=polarity_dark, unit=p.unit, aperture=offset_circle))
+
+ # it's safe to append these at the end since we compute a logical OR of opaque areas anyway.
+ self.primitives.extend(new_primitives)
@classmethod
def open(kls, filename, enable_includes=False, enable_include_dir=None):
@@ -406,14 +442,13 @@ class GraphicsState:
yield ApertureStmt(self.aperture_map[id(aperture)])
def set_current_point(self, point, unit=None):
- # FIXME use math.isclose for point comparisons here and elsewhere due to converted coords
- # FIXME maybe even calculate appropriate precision given file_settings.notation
+ # TODO calculate appropriate precision for math.isclose given file_settings.notation
if unit == 'inch':
point_mm = point[0]*25.4, point[1]*25.4
else:
point_mm = point
- if self.point != point_mm:
+ if not points_close(self.point, point_mm):
self.point = point_mm
yield MoveStmt(*point, unit=unit)
diff --git a/gerbonara/gerber/tests/image_support.py b/gerbonara/gerber/tests/image_support.py
index 9c80eec..bb202ab 100644
--- a/gerbonara/gerber/tests/image_support.py
+++ b/gerbonara/gerber/tests/image_support.py
@@ -92,7 +92,7 @@ def cleanup_clips(soup):
# canvas size. This is just wrong, so we just nuke the clip path from these SVG groups here.
#
# Apart from being graphically broken, this additionally causes very bad rendering performance.
- del group['clip-path'] # remove broken clip
+ del group['clip-path']
def gerber_difference(reference, actual, diff_out=None, svg_transform=None, size=(10,10)):
with tempfile.NamedTemporaryFile(suffix='.svg') as act_svg,\