summaryrefslogtreecommitdiff
path: root/gerbonara/gerber
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber')
-rw-r--r--gerbonara/gerber/aperture_macros/parse.py19
-rw-r--r--gerbonara/gerber/apertures.py126
-rw-r--r--gerbonara/gerber/cam.py1
-rw-r--r--gerbonara/gerber/gerber_statements.py2
-rw-r--r--gerbonara/gerber/graphic_primitives.py1
-rw-r--r--gerbonara/gerber/rs274x.py7
-rw-r--r--gerbonara/gerber/tests/image_support.py1
-rw-r--r--gerbonara/gerber/tests/test_rs274x.py16
8 files changed, 110 insertions, 63 deletions
diff --git a/gerbonara/gerber/aperture_macros/parse.py b/gerbonara/gerber/aperture_macros/parse.py
index 6eed381..86f2882 100644
--- a/gerbonara/gerber/aperture_macros/parse.py
+++ b/gerbonara/gerber/aperture_macros/parse.py
@@ -127,30 +127,29 @@ deg_per_rad = 180 / math.pi
class GenericMacros:
_generic_hole = lambda n: [
- ap.Circle(None, [0, var(n), 0, 0]),
- ap.CenterLine(None, [0, var(n), var(n+1), 0, 0, var(n+2) * -deg_per_rad])]
+ ap.Circle('mm', [0, var(n), 0, 0]),
+ ap.CenterLine('mm', [0, var(n), var(n+1), 0, 0, var(n+2) * -deg_per_rad])]
- # Initialize all these with "None" units so they inherit file units, and do not convert their arguments.
# NOTE: All generic macros have rotation values specified in **clockwise radians** like the rest of the user-facing
# API.
circle = ApertureMacro('GNC', [
- ap.Circle(None, [1, var(1), 0, 0, var(4) * -deg_per_rad]),
+ ap.Circle('mm', [1, var(1), 0, 0, var(4) * -deg_per_rad]),
*_generic_hole(2)])
rect = ApertureMacro('GNR', [
- ap.CenterLine(None, [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]),
+ ap.CenterLine('mm', [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]),
*_generic_hole(3) ])
# w must be larger than h
obround = ApertureMacro('GNO', [
- ap.CenterLine(None, [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]),
- ap.Circle(None, [1, var(2), +var(1)/2, 0, var(5) * -deg_per_rad]),
- ap.Circle(None, [1, var(2), -var(1)/2, 0, var(5) * -deg_per_rad]),
+ ap.CenterLine('mm', [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]),
+ ap.Circle('mm', [1, var(2), +var(1)/2, 0, var(5) * -deg_per_rad]),
+ ap.Circle('mm', [1, var(2), -var(1)/2, 0, var(5) * -deg_per_rad]),
*_generic_hole(3) ])
polygon = ApertureMacro('GNP', [
- ap.Polygon(None, [1, var(2), 0, 0, var(1), var(3) * -deg_per_rad]),
- ap.Circle(None, [0, var(4), 0, 0])])
+ ap.Polygon('mm', [1, var(2), 0, 0, var(1), var(3) * -deg_per_rad]),
+ ap.Circle('mm', [0, var(4), 0, 0])])
if __name__ == '__main__':
diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py
index 9e5c23a..2723c8d 100644
--- a/gerbonara/gerber/apertures.py
+++ b/gerbonara/gerber/apertures.py
@@ -1,6 +1,6 @@
import math
-from dataclasses import dataclass, replace, astuple, InitVar
+from dataclasses import dataclass, replace, fields, InitVar, KW_ONLY
from .aperture_macros.parse import GenericMacros
@@ -20,7 +20,17 @@ def strip_right(*args):
return args
+class Length:
+ def __init__(self, obj_type):
+ self.type = obj_type
+
+CONVERSION_FACTOR = {None: 1, 'mm': 25.4, 'inch': 1/25.4}
+
+@dataclass
class Aperture:
+ _ : KW_ONLY
+ unit : str = None
+
@property
def hole_shape(self):
if self.hole_rect_h is not None:
@@ -32,9 +42,26 @@ class Aperture:
def hole_size(self):
return (self.hole_dia, self.hole_rect_h)
- @property
- def params(self):
- return astuple(self)
+ def convert(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):
+ if f.kw_only:
+ continue
+
+ val = getattr(self, f.name)
+ if isinstance(f.type, Length):
+ val = self.convert(val, unit)
+ out.append(val)
+
+ return out
def flash(self, x, y):
return self.primitives(x, y)
@@ -43,16 +70,19 @@ class Aperture:
def equivalent_width(self):
raise ValueError('Non-circular aperture used in interpolation statement, line width is not properly defined.')
- def to_gerber(self):
+ def to_gerber(self, settings=None):
# Hack: The standard aperture shapes C, R, O do not have a rotation parameter. To make this API easier to use,
# we emulate this parameter. Our circle, rectangle and oblong classes below have a rotation parameter. Only at
# export time during to_gerber, this parameter is evaluated.
+ unit = settings.unit if settings else None
+ #print(f'aperture to gerber {self.unit=} {settings=} {unit=}')
actual_inst = self._rotated()
- params = 'X'.join(f'{float(par):.4}' for par in actual_inst.params if par is not None)
+ params = 'X'.join(f'{float(par):.4}' for par in actual_inst.params(unit) if par is not None)
return f'{actual_inst.gerber_shape_code},{params}'
def __eq__(self, other):
- return hasattr(other, to_gerber) and self.to_gerber() == other.to_gerber()
+ # We need to choose some unit here.
+ return hasattr(other, to_gerber) and self.to_gerber('mm') == other.to_gerber('mm')
def _rotate_hole_90(self):
if self.hole_rect_h is None:
@@ -65,9 +95,9 @@ class Aperture:
class CircleAperture(Aperture):
gerber_shape_code = 'C'
human_readable_shape = 'circle'
- diameter : float
- hole_dia : float = None
- hole_rect_h : float = None
+ diameter : Length(float)
+ hole_dia : Length(float) = None
+ hole_rect_h : Length(float) = None
rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber
def primitives(self, x, y, rotation):
@@ -89,21 +119,23 @@ class CircleAperture(Aperture):
return self.to_macro(self.rotation)
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.circle, self.params)
+ return ApertureMacroInstance(GenericMacros.circle, self.params(unit='mm'))
- @property
- def params(self):
- return strip_right(self.diameter, self.hole_dia, self.hole_rect_h)
+ def params(self, unit=None):
+ return strip_right(
+ self.convert(self.diameter, unit),
+ self.convert(self.hole_dia, unit),
+ self.convert(self.hole_rect_h, unit))
@dataclass
class RectangleAperture(Aperture):
gerber_shape_code = 'R'
human_readable_shape = 'rect'
- w : float
- h : float
- hole_dia : float = None
- hole_rect_h : float = None
+ w : Length(float)
+ h : Length(float)
+ hole_dia : Length(float) = None
+ hole_rect_h : Length(float) = None
rotation : float = 0 # radians
def primitives(self, x, y):
@@ -128,21 +160,28 @@ class RectangleAperture(Aperture):
def to_macro(self):
return ApertureMacroInstance(GenericMacros.rect,
- [self.w, self.h, self.hole_dia or 0, self.hole_rect_h or 0, self.rotation])
+ [self.convert(self.w, 'mm'),
+ self.convert(self.h, 'mm'),
+ self.convert(self.hole_dia, 'mm') or 0,
+ self.convert(self.hole_rect_h, 'mm') or 0,
+ self.rotation])
- @property
- def params(self):
- return strip_right(self.w, self.h, self.hole_dia, self.hole_rect_h)
+ def params(self, unit=None):
+ return strip_right(
+ self.convert(self.w, unit),
+ self.convert(self.h, unit),
+ self.convert(self.hole_dia, unit),
+ self.convert(self.hole_rect_h, unit))
@dataclass
class ObroundAperture(Aperture):
gerber_shape_code = 'O'
human_readable_shape = 'obround'
- w : float
- h : float
- hole_dia : float = None
- hole_rect_h : float = None
+ w : Length(float)
+ h : Length(float)
+ hole_dia : Length(float) = None
+ hole_rect_h : Length(float) = None
rotation : float = 0
def primitives(self, x, y):
@@ -165,20 +204,27 @@ class ObroundAperture(Aperture):
# generic macro only supports w > h so flip x/y if h > w
inst = self if self.w > self.h else replace(self, w=self.h, h=self.w, **_rotate_hole_90(self), rotation=self.rotation-90)
return ApertureMacroInstance(GenericMacros.obround,
- [inst.w, ints.h, inst.hole_dia, inst.hole_rect_h, inst.rotation])
+ [self.convert(inst.w, 'mm'),
+ self.convert(ints.h, 'mm'),
+ self.convert(inst.hole_dia, 'mm'),
+ self.convert(inst.hole_rect_h, 'mm'),
+ inst.rotation])
- @property
- def params(self):
- return strip_right(self.w, self.h, self.hole_dia, self.hole_rect_h)
+ def params(self, unit=None):
+ return strip_right(
+ self.convert(self.w, unit),
+ self.convert(self.h, unit),
+ self.convert(self.hole_dia, unit),
+ self.convert(self.hole_rect_h, unit))
@dataclass
class PolygonAperture(Aperture):
gerber_shape_code = 'P'
- diameter : float
+ diameter : Length(float)
n_vertices : int
rotation : float = 0
- hole_dia : float = None
+ hole_dia : Length(float) = None
def primitives(self, x, y):
return [ gp.RegularPolygon(x, y, diameter, n_vertices, rotation=self.rotation) ]
@@ -192,17 +238,16 @@ class PolygonAperture(Aperture):
return self
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.polygon, self.params)
+ return ApertureMacroInstance(GenericMacros.polygon, self.params('mm'))
- @property
- def params(self):
+ def params(self, unit=None):
rotation = self.rotation % (2*math.pi / self.n_vertices) if self.rotation is not None else None
if self.hole_dia is not None:
- return self.diameter, self.n_vertices, rotation, self.hole_dia
+ return self.convert(self.diameter, unit), self.n_vertices, rotation, self.convert(self.hole_dia, unit)
elif rotation is not None and not math.isclose(rotation, 0):
- return self.diameter, self.n_vertices, rotation
+ return self.convert(self.diameter, unit), self.n_vertices, rotation
else:
- return self.diameter, self.n_vertices
+ return self.convert(self.diameter, unit), self.n_vertices
@dataclass
class ApertureMacroInstance(Aperture):
@@ -235,8 +280,9 @@ class ApertureMacroInstance(Aperture):
hasattr(other, 'params') and self.params == other.params and \
hasattr(other, 'rotation') and self.rotation == other.rotation
- @property
- def params(self):
+ def params(self, unit=None):
+ # We ignore "unit" here as we convert the actual macro, not this instantiation.
+ # We do this because here we do not have information about which parameter has which physical units.
return tuple(self.parameters)
diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py
index fa38382..7988b44 100644
--- a/gerbonara/gerber/cam.py
+++ b/gerbonara/gerber/cam.py
@@ -92,6 +92,7 @@ class FileSettings:
# negative sign affects padding, so deal with it at the end...
sign = '-' if value < 0 else ''
+ # FIXME never use exponential notation here
num = format(abs(value), f'0{integer_digits+decimal_digits+1}.{decimal_digits}f').replace('.', '')
# Suppression...
diff --git a/gerbonara/gerber/gerber_statements.py b/gerbonara/gerber/gerber_statements.py
index 9d318a3..b47dfe3 100644
--- a/gerbonara/gerber/gerber_statements.py
+++ b/gerbonara/gerber/gerber_statements.py
@@ -83,7 +83,7 @@ class ApertureDefStmt(ParamStmt):
self.aperture = aperture
def to_gerber(self, settings=None):
- return f'%ADD{self.number}{self.aperture.to_gerber()}*%'
+ return f'%ADD{self.number}{self.aperture.to_gerber(settings)}*%'
def __str__(self):
return f'<AD aperture def for {str(self.aperture).strip("<>")}>'
diff --git a/gerbonara/gerber/graphic_primitives.py b/gerbonara/gerber/graphic_primitives.py
index de1a0bc..966cac1 100644
--- a/gerbonara/gerber/graphic_primitives.py
+++ b/gerbonara/gerber/graphic_primitives.py
@@ -10,6 +10,7 @@ from .gerber_statements import *
class GraphicPrimitive:
_ : KW_ONLY
polarity_dark : bool = True
+ unit : str = None
def rotate_point(x, y, angle, cx=0, cy=0):
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py
index b5fbfac..eea1cf7 100644
--- a/gerbonara/gerber/rs274x.py
+++ b/gerbonara/gerber/rs274x.py
@@ -165,9 +165,6 @@ class GerberFile(CamFile):
yield EofStmt()
- def to_gerber(self):
- return '\n'.join(self.generate_statements())
-
def __str__(self):
return f'<GerberFile with {len(self.apertures)} apertures, {len(self.objects)} objects>'
@@ -599,10 +596,10 @@ class GerberParser:
}
if (kls := aperture_classes.get(match['shape'])):
- new_aperture = kls(*modifiers)
+ new_aperture = kls(*modifiers, unit=self.file_settings.unit)
elif (macro := self.aperture_macros.get(match['shape'])):
- new_aperture = apertures.ApertureMacroInstance(macro, modifiers)
+ new_aperture = apertures.ApertureMacroInstance(macro, modifiers, unit=self.file_settings.unit)
else:
raise ValueError(f'Aperture shape "{match["shape"]}" is unknown')
diff --git a/gerbonara/gerber/tests/image_support.py b/gerbonara/gerber/tests/image_support.py
index 96bc357..23b829d 100644
--- a/gerbonara/gerber/tests/image_support.py
+++ b/gerbonara/gerber/tests/image_support.py
@@ -129,6 +129,7 @@ def image_difference(reference, actual, diff_out=None):
out = np.array(Image.open(actual)).astype(float)
ref, out = ref.mean(axis=2), out.mean(axis=2) # convert to grayscale
+ # FIXME blur images here before comparison to mitigate aliasing issue
delta = np.abs(out - ref).astype(float) / 255
if diff_out:
Image.fromarray((delta*255).astype(np.uint8), mode='L').save(diff_out)
diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py
index aec2174..0b061b1 100644
--- a/gerbonara/gerber/tests/test_rs274x.py
+++ b/gerbonara/gerber/tests/test_rs274x.py
@@ -118,9 +118,10 @@ def test_round_trip(temp_files, reference):
GerberFile.open(ref).save(tmp_gbr)
- mean, max = gerber_difference(ref, tmp_gbr, diff_out=tmp_png)
- assert mean < 1e-6
- assert max < 0.1
+ mean, _max, hist = gerber_difference(ref, tmp_gbr, diff_out=tmp_png)
+ assert mean < 5e-5
+ assert hist[9] == 0
+ assert hist[3:].sum() < 5e-5*hist.size
TEST_ANGLES = [90, 180, 270, 30, 1.5, 10, 360, 1024, -30, -90]
TEST_OFFSETS = [(0, 0), (100, 0), (0, 100), (2, 0), (10, 100)]
@@ -165,11 +166,12 @@ def test_rotation_center(temp_files, reference, angle, center):
# calculate circle center in SVG coordinates
size = (10, 10) # inches
cx, cy = to_gerbv_svg_units(center[0]), to_gerbv_svg_units(10, 'inch')-to_gerbv_svg_units(center[1], 'mm')
- mean, _max = gerber_difference(ref, tmp_gbr, diff_out=tmp_png,
+ mean, _max, hist = gerber_difference(ref, tmp_gbr, diff_out=tmp_png,
svg_transform=f'rotate({angle} {cx} {cy})',
size=size)
assert mean < 1e-3
- assert hist[9] == 0
+ assert hist[9] < 50
+ assert hist[3:].sum() < 1e-3*hist.size
@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning')
@pytest.mark.filterwarnings('ignore::SyntaxWarning')
@@ -205,13 +207,13 @@ def test_combined(temp_files, reference, angle, center, offset):
f = GerberFile.open(ref)
f.rotate(deg_to_rad(angle), center=center)
f.offset(*offset)
- f.save(tmp_gbr)
+ f.save(tmp_gbr, settings=FileSettings(unit=f.unit, number_format=(4,7)))
size = (10, 10) # inches
cx, cy = to_gerbv_svg_units(center[0]), to_gerbv_svg_units(10, 'inch')-to_gerbv_svg_units(center[1], 'mm')
dx, dy = to_gerbv_svg_units(offset[0]), -to_gerbv_svg_units(offset[1])
mean, _max, hist = gerber_difference(ref, tmp_gbr, diff_out=tmp_png,
- svg_transform=f'rotate({angle} {cx} {cy}) translate({dx} {dy})',
+ svg_transform=f'translate({dx} {dy}) rotate({angle} {cx} {cy})',
size=size)
assert mean < 1e-3
assert hist[9] < 100