aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--gerberex/am_expression.py4
-rw-r--r--gerberex/am_primitive.py16
-rw-r--r--gerberex/composition.py4
-rw-r--r--gerberex/excellon.py17
-rw-r--r--gerberex/gerber_statements.py115
-rw-r--r--gerberex/rs274x.py26
-rw-r--r--gerberex/statements.py70
-rw-r--r--gerberex/utility.py3
-rw-r--r--setup.py8
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/data/ref_drill_inch.txt43
-rw-r--r--tests/data/ref_drill_metric.txt45
-rw-r--r--tests/data/ref_dxf_metric.dxf404
-rw-r--r--tests/data/ref_dxf_mousebites.dxf344
-rw-r--r--tests/data/ref_gerber_inch.gtl79
-rw-r--r--tests/data/ref_gerber_metric.gtl84
-rw-r--r--tests/expects/RS2724x_offset.gtl79
-rw-r--r--tests/expects/RS2724x_rotate.gtl99
-rw-r--r--tests/expects/RS2724x_save.gtl79
-rw-r--r--tests/expects/RS2724x_to_inch.gtl79
-rw-r--r--tests/expects/RS2724x_to_metric.gtl79
-rw-r--r--tests/expects/dxf_offset.gtl42
-rw-r--r--tests/expects/dxf_rectangle_inch.gtl21
-rw-r--r--tests/expects/dxf_rectangle_metric.gtl21
-rw-r--r--tests/expects/dxf_rotate.gtl42
-rw-r--r--tests/expects/dxf_save_fill.gtl44
-rw-r--r--tests/expects/dxf_save_line.gtl42
-rw-r--r--tests/expects/dxf_save_mousebites.gtl29
-rw-r--r--tests/expects/dxf_save_mousebites.txt29
-rw-r--r--tests/expects/dxf_to_inch.gtl42
-rw-r--r--tests/expects/excellon_offset.txt43
-rw-r--r--tests/expects/excellon_rotate.txt43
-rw-r--r--tests/expects/excellon_save.txt43
-rw-r--r--tests/expects/excellon_to_inch.txt43
-rw-r--r--tests/expects/excellon_to_metric.txt43
-rw-r--r--tests/test_am_expression.py207
-rw-r--r--tests/test_dxf.py113
-rw-r--r--tests/test_excellon.py72
-rw-r--r--tests/test_rs274x.py69
-rw-r--r--tests/test_utility.py65
41 files changed, 2634 insertions, 100 deletions
diff --git a/README.md b/README.md
index a2c309d..f992edf 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ In order to fill closed shape, ```DM_FILL``` has to be set to ```draw_mode``` pr
import gerberex
dxf = gerberex.read('outline.dxf')
-dxf.draw_mode = gerberex.DxfFile.DM_FILL
+dxf.draw_mode = dxf.DM_FILL
dxf.write('outline.gml')
```
@@ -127,7 +127,7 @@ drill = gerberex.read('drill.txt')
ctx.merge(drill)
dxf = gerberex.read('mousebites.dxf')
-dxf.draw_mode = gerberex.DxfFile.DM_MOUSE_BITES
+dxf.draw_mode = dxf.DM_MOUSE_BITES
dxf.to_metric()
dxf.width = 0.5
dxf.pitch = 1
diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py
index 7aa95c8..b758df1 100644
--- a/gerberex/am_expression.py
+++ b/gerberex/am_expression.py
@@ -49,7 +49,7 @@ class AMConstantExpression(AMExpression):
return self
def to_gerber(self, settings=None):
- return str(self._value)
+ return '%.6g' % self._value
def to_instructions(self):
return [(OpCode.PUSH, self._value)]
@@ -179,5 +179,3 @@ def eval_macro(instructions):
elif opcode == OpCode.PRIM:
yield (argument, stack)
stack = []
-
-
diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py
index 82370f6..df55573 100644
--- a/gerberex/am_primitive.py
+++ b/gerberex/am_primitive.py
@@ -117,14 +117,14 @@ class AMVectorLinePrimitiveDef(AMPrimitiveDef):
self.start_x = self.start_x.to_inch().optimize()
self.start_y = self.start_y.to_inch().optimize()
self.end_x = self.end_x.to_inch().optimize()
- self.end_y = self.end_x.to_inch().optimize()
+ self.end_y = self.end_y.to_inch().optimize()
def to_metric(self):
self.width = self.width.to_metric().optimize()
self.start_x = self.start_x.to_metric().optimize()
self.start_y = self.start_y.to_metric().optimize()
self.end_x = self.end_x.to_metric().optimize()
- self.end_y = self.end_x.to_metric().optimize()
+ self.end_y = self.end_y.to_metric().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
@@ -197,11 +197,11 @@ class AMCenterLinePrimitiveDef(AMPrimitiveDef):
class AMOutlinePrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
- num_points = modifiers[1] + 1
+ num_points = int(modifiers[1].value + 1)
code = code
exposure = 'on' if modifiers[0].value == 1 else 'off'
- addrs = modifiers[2:num_points * 2]
- rotation = modifiers[3 + num_points * 2]
+ addrs = modifiers[2:num_points * 2 + 2]
+ rotation = modifiers[2 + num_points * 2]
return cls(code, exposure, addrs, rotation)
def __init__(self, code, exposure, addrs, rotation):
@@ -209,10 +209,10 @@ class AMOutlinePrimitiveDef(AMPrimitiveDef):
self.addrs = addrs
def to_inch(self):
- self.addrs = [i.to_inch() for i in self.addrs]
+ self.addrs = [i.to_inch().optimize() for i in self.addrs]
def to_metric(self):
- self.addrs = [i.to_metric() for i in self.addrs]
+ self.addrs = [i.to_metric().optimize() for i in self.addrs]
def to_gerber(self, settings=None):
def strs():
@@ -262,7 +262,7 @@ class AMPolygonPrimitiveDef(AMPrimitiveDef):
def to_metric(self):
self.x = self.x.to_metric().optimize()
self.y = self.y.to_metric().optimize()
- self.diameter = self.diameter.to_inch().optimize()
+ self.diameter = self.diameter.to_metric().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
diff --git a/gerberex/composition.py b/gerberex/composition.py
index 73f5702..7abf090 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -202,8 +202,8 @@ class DrillComposition(Composition):
file.to_inch()
if not self.header1_statements:
- self.header1_statements = file.header
- self.header2_statements = file.header2
+ self.header1_statements = [file.header]
+ self.header2_statements = [file.header2]
tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width))
self.dxf_statements.append((tool.number, file.statements))
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 90d6742..b72b95b 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -6,6 +6,7 @@
from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile)
from gerber.excellon_statements import UnitStmt
from gerber.cam import FileSettings
+from gerber.utils import inch, metric
from gerberex.utility import rotate
def loads(data, filename=None, settings=None, tools=None, format=None):
@@ -33,6 +34,19 @@ class ExcellonFileEx(ExcellonFile):
return
for hit in self.hits:
hit.position = rotate(hit.position[0], hit.position[1], angle, center)
+
+ def to_inch(self):
+ if self.units == 'metric':
+ super(ExcellonFileEx, self).to_inch()
+ for hit in self.hits:
+ hit.position = (inch(hit.position[0]), inch(hit.position[1]))
+
+ def to_metric(self):
+ if self.units == 'inch':
+ super(ExcellonFileEx, self).to_metric()
+ for hit in self.hits:
+ hit.position = (metric(hit.position[0]), metric(hit.position[1]))
+
class UnitStmtEx(UnitStmt):
@classmethod
@@ -43,7 +57,8 @@ class UnitStmtEx(UnitStmt):
super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs)
def to_excellon(self, settings=None):
+ format = settings.format if settings else self.format
stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
'LZ' if self.zeros == 'leading' else 'TZ',
- '0' * self.format[0], '0' * self.format[1])
+ '0' * format[0], '0' * format[1])
return stmt
diff --git a/gerberex/gerber_statements.py b/gerberex/gerber_statements.py
new file mode 100644
index 0000000..c2eb565
--- /dev/null
+++ b/gerberex/gerber_statements.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+from gerber.gerber_statements import AMParamStmt, ADParamStmt
+from gerber.utils import inch, metric
+from gerberex.am_primitive import to_primitive_defs
+
+class AMParamStmtEx(AMParamStmt):
+ @classmethod
+ def from_stmt(cls, stmt):
+ return cls(stmt.param, stmt.name, stmt.macro, stmt.units)
+
+ @classmethod
+ def circle(cls, name, units):
+ return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0', units)
+
+ @classmethod
+ def rectangle(cls, name, units):
+ return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0', units)
+
+ @classmethod
+ def landscape_obround(cls, name, units):
+ return cls(
+ 'AM', name,
+ '$4=$1-$2*'
+ '$5=$1-$4*'
+ '21,1,$5,$2,0,0,0*'
+ '1,1,$4,$4/2,0,0*'
+ '1,1,$4,-$4/2,0,0*'
+ '1,0,$3,0,0,0', units)
+
+ @classmethod
+ def portrate_obround(cls, name, units):
+ return cls(
+ 'AM', name,
+ '$4=$2-$1*'
+ '$5=$2-$4*'
+ '21,1,$1,$5,0,0,0*'
+ '1,1,$4,0,$4/2,0*'
+ '1,1,$4,0,-$4/2,0*'
+ '1,0,$3,0,0,0', units)
+
+ @classmethod
+ def polygon(cls, name, units):
+ return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0', units)
+
+ def __init__(self, param, name, macro, units):
+ super(AMParamStmtEx, self).__init__(param, name, macro)
+ self.units = units
+ self.primitive_defs = list(to_primitive_defs(self.instructions))
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ for p in self.primitive_defs:
+ p.to_inch()
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ for p in self.primitive_defs:
+ p.to_metric()
+
+ def to_gerber(self, settings = None):
+ def plist():
+ for p in self.primitive_defs:
+ yield p.to_gerber(settings)
+ return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))
+
+ def rotate(self, angle, center=None):
+ for primitive_def in self.primitive_defs:
+ primitive_def.rotate(angle, center)
+
+class ADParamStmtEx(ADParamStmt):
+ GEOMETRIES = {
+ 'C': [0,1],
+ 'R': [0,1,2],
+ 'O': [0,1,2],
+ 'P': [0,3],
+ }
+
+ @classmethod
+ def from_stmt(cls, stmt):
+ modstr = ','.join([
+ 'X'.join(['{0}'.format(x) for x in modifier])
+ for modifier in stmt.modifiers])
+ return cls(stmt.param, stmt.d, stmt.shape, modstr, stmt.units)
+
+ def __init__(self, param, d, shape, modifiers, units):
+ super(ADParamStmtEx, self).__init__(param, d, shape, modifiers)
+ self.units = units
+
+ def to_inch(self):
+ if self.units == 'inch':
+ return
+ self.units = 'inch'
+ if self.shape in self.GEOMETRIES:
+ indices = self.GEOMETRIES[self.shape]
+ self.modifiers = [tuple([
+ inch(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \
+ for i in range(len(self.modifiers[0]))
+ ])]
+
+ def to_metric(self):
+ if self.units == 'metric':
+ return
+ self.units = 'metric'
+ if self.shape in self.GEOMETRIES:
+ indices = self.GEOMETRIES[self.shape]
+ self.modifiers = [tuple([
+ metric(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \
+ for i in range(len(self.modifiers[0]))
+ ])]
diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
index 4eb317d..13d3421 100644
--- a/gerberex/rs274x.py
+++ b/gerberex/rs274x.py
@@ -5,7 +5,7 @@
import gerber.rs274x
from gerber.gerber_statements import ADParamStmt, CoordStmt
-from gerberex.statements import AMParamStmt, AMParamStmtEx
+from gerberex.gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx
from gerberex.utility import rotate
class GerberFile(gerber.rs274x.GerberFile):
@@ -17,6 +17,8 @@ class GerberFile(gerber.rs274x.GerberFile):
def swap_statement(statement):
if isinstance(statement, AMParamStmt) and not isinstance(statement, AMParamStmtEx):
return AMParamStmtEx.from_stmt(statement)
+ elif isinstance(statement, ADParamStmt) and not isinstance(statement, AMParamStmtEx):
+ return ADParamStmtEx.from_stmt(statement)
else:
return statement
statements = [swap_statement(statement) for statement in gerber_file.statements]
@@ -26,6 +28,18 @@ class GerberFile(gerber.rs274x.GerberFile):
def __init__(self, statements, settings, primitives, apertures, filename=None):
super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename)
+ def offset(self, x_offset=0, y_offset=0):
+ for statement in self.statements:
+ if isinstance(statement, CoordStmt):
+ if statement.x is not None:
+ statement.x += x_offset
+ if statement.y is not None:
+ statement.y += y_offset
+ else:
+ statement.offset(x_offset, y_offset)
+ for primitive in self.primitives:
+ primitive.offset(x_offset, y_offset)
+
def rotate(self, angle, center=(0,0)):
if angle % 360 == 0:
return
@@ -84,7 +98,7 @@ class GerberFile(gerber.rs274x.GerberFile):
while name in macros:
name = '%s_%d' % (macro_def[0], num)
num += 1
- self.statements.insert(insert_point, macro_def[1](name))
+ self.statements.insert(insert_point, macro_def[1](name, self.units))
macro_defs[idx] = (name, macro_def[1])
for idx in range(insert_point, last_aperture + len(macro_defs) + 1):
statement = self.statements[idx]
@@ -92,10 +106,10 @@ class GerberFile(gerber.rs274x.GerberFile):
if statement.shape == 'R':
statement.shape = macro_defs[RECTANGLE][0]
elif statement.shape == 'O':
- x = statement.modifiers[0] \
- if len(statement.modifiers) > 0 else 0
- y = statement.modifiers[1] \
- if len(statement.modifiers) > 1 else 0
+ x = statement.modifiers[0][0] \
+ if len(statement.modifiers[0]) > 0 else 0
+ y = statement.modifiers[0][1] \
+ if len(statement.modifiers[0]) > 1 else 0
statement.shape = macro_defs[LANDSCAPE_OBROUND][0] \
if x > y else macro_defs[PORTRATE_OBROUND][0]
elif statement.shape == 'P':
diff --git a/gerberex/statements.py b/gerberex/statements.py
deleted file mode 100644
index c41acb9..0000000
--- a/gerberex/statements.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
-
-from gerber.gerber_statements import AMParamStmt
-from gerberex.am_primitive import to_primitive_defs
-
-class AMParamStmtEx(AMParamStmt):
- @classmethod
- def from_stmt(cls, stmt):
- return cls(stmt.param, stmt.name, stmt.macro)
-
- @classmethod
- def circle(cls, name):
- return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0')
-
- @classmethod
- def rectangle(cls, name):
- return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0')
-
- @classmethod
- def landscape_obround(cls, name):
- return cls(
- 'AM', name,
- '$4=$1-$2*'
- '21,1,$1-$4,$2,0,0,0*'
- '1,1,$4,$4/2,0,0*'
- '1,1,$4,-$4/2,0,0*'
- '1,0,$3,0,0,0')
-
- @classmethod
- def portrate_obround(cls, name):
- return cls(
- 'AM', name,
- '$4=$2-$1*'
- '21,1,$1,$2-$4,0,0,0*'
- '1,1,$4,0,$4/2,0*'
- '1,1,$4,0,-$4/2,0*'
- '1,0,$3,0,0,0')
-
- @classmethod
- def polygon(cls, name):
- return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0')
-
- def __init__(self, param, name, macro):
- super(AMParamStmtEx, self).__init__(param, name, macro)
- self.primitive_defs = list(to_primitive_defs(self.instructions))
-
- def to_inch(self):
- if self.units == 'metric':
- self.units = 'inch'
- for p in self.primitive_defs:
- p.to_inch()
-
- def to_metric(self):
- if self.units == 'inch':
- self.units = 'metric'
- for p in self.primitive_defs:
- p.to_metric()
-
- def to_gerber(self, settings = None):
- def plist():
- for p in self.primitive_defs:
- yield p.to_gerber(settings)
- return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))
-
- def rotate(self, angle, center=None):
- for primitive_def in self.primitive_defs:
- primitive_def.rotate(angle, center)
diff --git a/gerberex/utility.py b/gerberex/utility.py
index f90df96..4c89fa6 100644
--- a/gerberex/utility.py
+++ b/gerberex/utility.py
@@ -13,8 +13,7 @@ def rotate(x, y, angle, center):
sin(angle) * x0 + cos(angle) * y0 + center[1])
def is_equal_value(a, b, error_range=0):
- return a - b <= error_range and a - b >= -error_range
-
+ return (a - b) * (a - b) <= error_range * error_range
def is_equal_point(a, b, error_range=0):
return is_equal_value(a[0], b[0], error_range) and \
diff --git a/setup.py b/setup.py
index eb99d92..72d9c18 100644
--- a/setup.py
+++ b/setup.py
@@ -1,11 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
import os
@@ -14,7 +10,7 @@ def read(fname):
METADATA = {
'name': 'pcb-tools-extension',
- 'version': "0.1.2",
+ 'version': "0.1.4",
'author': 'Hiroshi Murayama <opiopan@gmail.com>',
'author_email': "opiopan@gmail.com",
'description': ("Extension for pcb-tools package to panelize gerber files"),
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/data/ref_drill_inch.txt b/tests/data/ref_drill_inch.txt
new file mode 100644
index 0000000..8d31df0
--- /dev/null
+++ b/tests/data/ref_drill_inch.txt
@@ -0,0 +1,43 @@
+M48
+FMAT,2
+ICI,OFF
+INCH,TZ,00.0000
+M72
+T01C0.0039
+T02C0.0078
+%
+T01
+X1969Y3740
+X2047Y3740
+X2126Y3740
+X2205Y3740
+X2283Y3740
+X2362Y3740
+X2441Y3740
+X2520Y3740
+X2598Y3740
+X2677Y3740
+X2756Y3740
+X2835Y3740
+X2913Y3740
+X2992Y3740
+X3071Y3740
+X3150Y3740
+X3228Y3740
+X3307Y3740
+X3386Y3740
+X3465Y3740
+X3543Y3740
+T02
+X1969Y197
+X2126Y197
+X2283Y197
+X2441Y197
+X2598Y197
+X2756Y197
+X2913Y197
+X3071Y197
+X3228Y197
+X3386Y197
+X3543Y197
+M30
diff --git a/tests/data/ref_drill_metric.txt b/tests/data/ref_drill_metric.txt
new file mode 100644
index 0000000..87f20f5
--- /dev/null
+++ b/tests/data/ref_drill_metric.txt
@@ -0,0 +1,45 @@
+M48
+FMAT,2
+ICI,OFF
+METRIC,TZ,000.000
+M71
+T01C0.100
+T02C0.200
+%
+T01
+X5000Y9500
+X5200Y9500
+X5400Y9500
+X5600Y9500
+X5800Y9500
+X6000Y9500
+X6200Y9500
+X6400Y9500
+X6600Y9500
+X6800Y9500
+X7000Y9500
+X7200Y9500
+X7400Y9500
+X7600Y9500
+X7800Y9500
+X8000Y9500
+X8200Y9500
+X8400Y9500
+X8600Y9500
+X8800Y9500
+X9000Y9500
+
+T02
+X5000Y500
+X5400Y500
+X5800Y500
+X6200Y500
+X6600Y500
+X7000Y500
+X7400Y500
+X7800Y500
+X8200Y500
+X8600Y500
+X9000Y500
+
+M30
diff --git a/tests/data/ref_dxf_metric.dxf b/tests/data/ref_dxf_metric.dxf
new file mode 100644
index 0000000..89af1e7
--- /dev/null
+++ b/tests/data/ref_dxf_metric.dxf
@@ -0,0 +1,404 @@
+0
+SECTION
+2
+HEADER
+9
+$INSUNITS
+70
+4
+9
+$ACADVER
+1
+AC1014
+9
+$HANDSEED
+5
+FFFF
+0
+ENDSEC
+0
+SECTION
+2
+TABLES
+0
+TABLE
+2
+VPORT
+5
+8
+100
+AcDbSymbolTable
+0
+ENDTAB
+0
+TABLE
+2
+LTYPE
+5
+5
+100
+AcDbSymbolTable
+0
+LTYPE
+5
+14
+100
+AcDbSymbolTableRecord
+100
+AcDbLinetypeTableRecord
+2
+BYBLOCK
+70
+0
+0
+LTYPE
+5
+15
+100
+AcDbSymbolTableRecord
+100
+AcDbLinetypeTableRecord
+2
+BYLAYER
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+LAYER
+5
+2
+100
+AcDbSymbolTable
+70
+2
+0
+LAYER
+5
+50
+100
+AcDbSymbolTableRecord
+100
+AcDbLayerTableRecord
+2
+0
+70
+0
+6
+CONTINUOUS
+0
+ENDTAB
+0
+TABLE
+2
+STYLE
+5
+3
+100
+AcDbSymbolTable
+70
+1
+0
+STYLE
+5
+11
+100
+AcDbSymbolTableRecord
+100
+AcDbTextStyleTableRecord
+2
+STANDARD
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+VIEW
+5
+6
+100
+AcDbSymbolTable
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+UCS
+5
+7
+100
+AcDbSymbolTable
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+APPID
+5
+9
+100
+AcDbSymbolTable
+70
+2
+0
+APPID
+5
+12
+100
+AcDbSymbolTableRecord
+100
+AcDbRegAppTableRecord
+2
+ACAD
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+DIMSTYLE
+5
+A
+100
+AcDbSymbolTable
+70
+1
+0
+ENDTAB
+0
+TABLE
+2
+BLOCK_RECORD
+5
+1
+100
+AcDbSymbolTable
+70
+1
+0
+BLOCK_RECORD
+5
+1F
+100
+AcDbSymbolTableRecord
+100
+AcDbBlockTableRecord
+2
+*MODEL_SPACE
+0
+BLOCK_RECORD
+5
+1B
+100
+AcDbSymbolTableRecord
+100
+AcDbBlockTableRecord
+2
+*PAPER_SPACE
+0
+ENDTAB
+0
+ENDSEC
+0
+SECTION
+2
+BLOCKS
+0
+BLOCK
+5
+20
+100
+AcDbEntity
+100
+AcDbBlockBegin
+2
+*MODEL_SPACE
+0
+ENDBLK
+5
+21
+100
+AcDbEntity
+100
+AcDbBlockEnd
+0
+BLOCK
+5
+1C
+100
+AcDbEntity
+100
+AcDbBlockBegin
+2
+*PAPER_SPACE
+0
+ENDBLK
+5
+1D
+100
+AcDbEntity
+100
+AcDbBlockEnd
+0
+ENDSEC
+0
+SECTION
+2
+ENTITIES
+0
+LWPOLYLINE
+5
+100
+100
+AcDbEntity
+8
+0
+100
+AcDbPolyline
+90
+8
+70
+1
+43
+0.0
+10
+9
+20
+0
+10
+1
+20
+0
+42
+-0.41421356237309515
+10
+0
+20
+0.99999999999999978
+10
+6.9388939039072284e-16
+20
+9
+42
+-0.41421356237309548
+10
+0.99999999999999978
+20
+10
+10
+9
+20
+10
+42
+-0.41421356237309509
+10
+10
+20
+9
+10
+10
+20
+1
+42
+-0.41421356237309548
+0
+CIRCLE
+5
+101
+100
+AcDbEntity
+8
+0
+100
+AcDbCircle
+10
+0.61705708382705282
+20
+5
+30
+0
+40
+0.29999999999999999
+0
+LWPOLYLINE
+5
+102
+100
+AcDbEntity
+8
+0
+100
+AcDbPolyline
+90
+3
+70
+1
+43
+0.0
+10
+0.91705708382705309
+20
+7.5106817728417301
+42
+-0.67748879940688445
+10
+0.39955725374872897
+20
+7.3040569342673214
+10
+0.61705708382705282
+20
+7.5106817728417301
+0
+ENDSEC
+0
+SECTION
+2
+OBJECTS
+0
+DICTIONARY
+5
+C
+100
+AcDbDictionary
+3
+ACAD_GROUP
+350
+D
+3
+ACAD_MLINESTYLE
+350
+17
+0
+DICTIONARY
+5
+D
+100
+AcDbDictionary
+0
+DICTIONARY
+5
+1A
+330
+C
+100
+AcDbDictionary
+0
+DICTIONARY
+5
+17
+100
+AcDbDictionary
+0
+ENDSEC
+0
+EOF
diff --git a/tests/data/ref_dxf_mousebites.dxf b/tests/data/ref_dxf_mousebites.dxf
new file mode 100644
index 0000000..fcc56a5
--- /dev/null
+++ b/tests/data/ref_dxf_mousebites.dxf
@@ -0,0 +1,344 @@
+0
+SECTION
+2
+HEADER
+9
+$INSUNITS
+70
+4
+9
+$ACADVER
+1
+AC1014
+9
+$HANDSEED
+5
+FFFF
+0
+ENDSEC
+0
+SECTION
+2
+TABLES
+0
+TABLE
+2
+VPORT
+5
+8
+100
+AcDbSymbolTable
+0
+ENDTAB
+0
+TABLE
+2
+LTYPE
+5
+5
+100
+AcDbSymbolTable
+0
+LTYPE
+5
+14
+100
+AcDbSymbolTableRecord
+100
+AcDbLinetypeTableRecord
+2
+BYBLOCK
+70
+0
+0
+LTYPE
+5
+15
+100
+AcDbSymbolTableRecord
+100
+AcDbLinetypeTableRecord
+2
+BYLAYER
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+LAYER
+5
+2
+100
+AcDbSymbolTable
+70
+2
+0
+LAYER
+5
+50
+100
+AcDbSymbolTableRecord
+100
+AcDbLayerTableRecord
+2
+0
+70
+0
+6
+CONTINUOUS
+0
+ENDTAB
+0
+TABLE
+2
+STYLE
+5
+3
+100
+AcDbSymbolTable
+70
+1
+0
+STYLE
+5
+11
+100
+AcDbSymbolTableRecord
+100
+AcDbTextStyleTableRecord
+2
+STANDARD
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+VIEW
+5
+6
+100
+AcDbSymbolTable
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+UCS
+5
+7
+100
+AcDbSymbolTable
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+APPID
+5
+9
+100
+AcDbSymbolTable
+70
+2
+0
+APPID
+5
+12
+100
+AcDbSymbolTableRecord
+100
+AcDbRegAppTableRecord
+2
+ACAD
+70
+0
+0
+ENDTAB
+0
+TABLE
+2
+DIMSTYLE
+5
+A
+100
+AcDbSymbolTable
+70
+1
+0
+ENDTAB
+0
+TABLE
+2
+BLOCK_RECORD
+5
+1
+100
+AcDbSymbolTable
+70
+1
+0
+BLOCK_RECORD
+5
+1F
+100
+AcDbSymbolTableRecord
+100
+AcDbBlockTableRecord
+2
+*MODEL_SPACE
+0
+BLOCK_RECORD
+5
+1B
+100
+AcDbSymbolTableRecord
+100
+AcDbBlockTableRecord
+2
+*PAPER_SPACE
+0
+ENDTAB
+0
+ENDSEC
+0
+SECTION
+2
+BLOCKS
+0
+BLOCK
+5
+20
+100
+AcDbEntity
+100
+AcDbBlockBegin
+2
+*MODEL_SPACE
+0
+ENDBLK
+5
+21
+100
+AcDbEntity
+100
+AcDbBlockEnd
+0
+BLOCK
+5
+1C
+100
+AcDbEntity
+100
+AcDbBlockBegin
+2
+*PAPER_SPACE
+0
+ENDBLK
+5
+1D
+100
+AcDbEntity
+100
+AcDbBlockEnd
+0
+ENDSEC
+0
+SECTION
+2
+ENTITIES
+0
+LINE
+5
+100
+100
+AcDbEntity
+8
+0
+100
+AcDbLine
+10
+0.99999999999999933
+20
+9.0000000000000018
+30
+0
+11
+0.99999999999999967
+21
+0.99999999999999967
+31
+0
+0
+LINE
+5
+101
+100
+AcDbEntity
+8
+0
+100
+AcDbLine
+10
+5
+20
+9.0000000000000018
+30
+0
+11
+5
+21
+0.99999999999999967
+31
+0
+0
+ENDSEC
+0
+SECTION
+2
+OBJECTS
+0
+DICTIONARY
+5
+C
+100
+AcDbDictionary
+3
+ACAD_GROUP
+350
+D
+3
+ACAD_MLINESTYLE
+350
+17
+0
+DICTIONARY
+5
+D
+100
+AcDbDictionary
+0
+DICTIONARY
+5
+1A
+330
+C
+100
+AcDbDictionary
+0
+DICTIONARY
+5
+17
+100
+AcDbDictionary
+0
+ENDSEC
+0
+EOF
diff --git a/tests/data/ref_gerber_inch.gtl b/tests/data/ref_gerber_inch.gtl
new file mode 100644
index 0000000..3ec60d8
--- /dev/null
+++ b/tests/data/ref_gerber_inch.gtl
@@ -0,0 +1,79 @@
+%MOIN*%
+%FSLAX25Y25*%
+%INTop Layer*%
+%IPPOS*%
+%AMCOMP*
+20,1,0.00787402,0,0.00393701,0.015748,0.00393701,$1*
+21,1,0.015748,0.00787402,-0.00787402,-0.00393701,$1*
+1,1,0.015748,-0.0472441,0,$1*
+4,1,4,0.0472441,0,0.0551181,-0.00787402,0.0472441,-0.015748,0.0393701,-0.00787402,0.0472441,0,$1*
+5,1,6,0.0472441,0.00787402,0.015748,$1*
+6,-0.0275591,0,0.019685,0.0019685,0.00590551,2,0.0019685,0.023622,$1*
+7,0.0275591,0,0.023622,0.019685,0.00590551,$1*%
+%ADD10C,0.0003937*%
+%ADD11C,0.03937X0.01575*%
+%ADD12R,0.03937X0.01969X0.007874*%
+%ADD13O,0.03937X0.01969X0.007874*%
+%ADD14O,0.01969X0.03937X0.007874*%
+%ADD15P,0.03937X5X90X0.007874*%
+%ADD16COMP,0*%
+%ADD17COMP,45*%
+%ADD18COMP,-45*%
+G75*
+%LPD*%
+D10*
+G01*
+X3937Y0D02*
+X35433Y0D01*
+G03*
+X39370Y3937I0J3937D01*
+G01*
+X39370Y35433D01*
+G03*
+X35433Y39370I-3937J0D01*
+G01*
+X3937Y39370D01*
+G03*
+X0Y35433I0J-3937D01*
+G01*
+X0Y3937D01*
+G03*
+X3937Y0I3937J0D01*
+G01*
+G36*
+G01*
+X17717Y3937D02*
+X19685Y3937D01*
+G03*
+X21654Y5906I0J1969D01*
+G01*
+X21654Y33465D01*
+G03*
+X19685Y35433I-1969J0D01*
+G01*
+X17717Y35433D01*
+G03*
+X15748Y33465I0J-1969D01*
+G01*
+X15748Y5906D01*
+G03*
+X17717Y3937I1969J0D01*
+G01*
+G37*
+D11*
+X9843Y3937D03*
+D12*
+X9843Y11811D03*
+D13*
+X9843Y19685D03*
+D14*
+X9843Y27559D03*
+D15*
+X9843Y35433D03*
+D16*
+X29528Y19685D03*
+D17*
+X29528Y29528D03*
+D18*
+X29528Y9843D03*
+M02*
diff --git a/tests/data/ref_gerber_metric.gtl b/tests/data/ref_gerber_metric.gtl
new file mode 100644
index 0000000..8dfbdd4
--- /dev/null
+++ b/tests/data/ref_gerber_metric.gtl
@@ -0,0 +1,84 @@
+%MOMM*%
+%FSLAX34Y34*%
+%INTop Layer*%
+%IPPOS*%
+%AMCOMP*
+20,1,0.2,0,0.1,0.4,0.1,$1*
+21,1,0.4,0.2,-0.2,-0.1,$1*
+1,1,0.4,-1.2,0,$1*
+4,1,4,1.2,0,1.4,-0.2,1.2,-0.4,1,-0.2,1.2,0,$1*
+5,1,6,1.2,0.2,0.4,$1*
+6,-0.7,0,0.5,0.05,0.15,2,0.05,0.6,$1*
+7,0.7,0,0.6,0.5,0.15,$1*%
+%ADD10C,0.01*%
+%ADD11C,1X0.4*%
+%ADD12R,1X0.5X0.2*%
+%ADD13O,1X0.5X0.2*%
+%ADD14O,0.5X1X0.2*%
+%ADD15P,1X5X90X0.2*%
+%ADD16COMP,0*%
+%ADD17COMP,45*%
+%ADD18COMP,-45*%
+G75*
+%LPD*%
+
+D10*
+G01*
+X10000Y0D02*
+X90000Y0D01*
+G03*
+X100000Y10000I0J10000D01*
+G01*
+X100000Y90000D01*
+G03*
+X90000Y100000I-10000J0D01*
+G01*
+X10000Y100000D01*
+G03*
+X0Y90000I0J-10000D01*
+G01*
+X0Y10000D01*
+G03*
+X10000Y0I10000J0D01*
+G01*
+
+G36*
+G01*
+X45000Y10000D02*
+X50000Y10000D01*
+G03*
+X55000Y15000I0J5000D01*
+G01*
+X55000Y85000D01*
+G03*
+X50000Y90000I-5000J0D01*
+G01*
+X45000Y90000D01*
+G03*
+X40000Y85000I0J-5000D01*
+G01*
+X40000Y15000D01*
+G03*
+X45000Y10000I5000J0D01*
+G01*
+G37*
+
+D11*
+X25000Y10000D03*
+D12*
+X25000Y30000D03*
+D13*
+X25000Y50000D03*
+D14*
+X25000Y70000D03*
+D15*
+X25000Y90000D03*
+
+D16*
+X75000Y50000D03*
+D17*
+X75000Y75000D03*
+D18*
+X75000Y25000D03*
+
+M02*
diff --git a/tests/expects/RS2724x_offset.gtl b/tests/expects/RS2724x_offset.gtl
new file mode 100644
index 0000000..9f15f4c
--- /dev/null
+++ b/tests/expects/RS2724x_offset.gtl
@@ -0,0 +1,79 @@
+%MOMM*%
+%FSLAX34Y34*%
+%INTop Layer*%
+%IPPOS*%
+%AMCOMP*
+20,1,0.2,0,0.1,0.4,0.1,$1*
+21,1,0.4,0.2,-0.2,-0.1,$1*
+1,1,0.4,-1.2,0,$1*
+4,1,4,1.2,0,1.4,-0.2,1.2,-0.4,1,-0.2,1.2,0,$1*
+5,1,6,1.2,0.2,0.4,$1*
+6,-0.7,0,0.5,0.05,0.15,2,0.05,0.6,$1*
+7,0.7,0,0.6,0.5,0.15,$1*%
+%ADD10C,0.01*%
+%ADD11C,1X0.4*%
+%ADD12R,1X0.5X0.2*%
+%ADD13O,1X0.5X0.2*%
+%ADD14O,0.5X1X0.2*%
+%ADD15P,1X5X90X0.2*%
+%ADD16COMP,0*%
+%ADD17COMP,45*%
+%ADD18COMP,-45*%
+G75*
+%LPD*%
+D10*
+G01*
+X120000Y50000D02*
+X200000Y50000D01*
+G03*
+X210000Y60000I0J10000D01*
+G01*
+X210000Y140000D01*
+G03*
+X200000Y150000I-10000J0D01*
+G01*
+X120000Y150000D01*
+G03*
+X110000Y140000I0J-10000D01*
+G01*
+X110000Y60000D01*
+G03*
+X120000Y50000I10000J0D01*
+G01*
+G36*
+G01*
+X155000Y60000D02*
+X160000Y60000D01*
+G03*
+X165000Y65000I0J5000D01*
+G01*
+X165000Y135000D01*
+G03*
+X160000Y140000I-5000J0D01*
+G01*
+X155000Y140000D01*
+G03*
+X150000Y135000I0J-5000D01*
+G01*
+X150000Y65000D01*
+G03*
+X155000Y60000I5000J0D01*
+G01*
+G37*
+D11*
+X135000Y60000D03*
+D12*
+X135000Y80000D03*
+D13*
+X135000Y100000D03*
+D14*
+X135000Y120000D03*
+D15*
+X135000Y140000D03*
+D16*
+X185000Y100000D03*
+D17*
+X185000Y125000D03*
+D18*
+X185000Y75000D03*
+M02*
diff --git a/tests/expects/RS2724x_rotate.gtl b/tests/expects/RS2724x_rotate.gtl
new file mode 100644
index 0000000..0a2d1aa
--- /dev/null
+++ b/tests/expects/RS2724x_rotate.gtl
@@ -0,0 +1,99 @@
+%MOMM*%
+%FSLAX34Y34*%
+%INTop Layer*%
+%IPPOS*%
+%AMCOMP*
+20,1,0.2,0,0.1,0.4,0.1,($1)+(20)*
+21,1,0.4,0.2,-0.2,-0.1,($1)+(20)*
+1,1,0.4,-1.2,0,($1)+(20)*
+4,1,4,1.2,0,1.4,-0.2,1.2,-0.4,1,-0.2,1.2,0,($1)+(20)*
+5,1,6,1.2,0.2,0.4,($1)+(20)*
+6,-0.7,0,0.5,0.05,0.15,2,0.05,0.6,($1)+(20)*
+7,0.7,0,0.6,0.5,0.15,($1)+(20)*%
+%AMMACP*
+5,1,$2,0,0,$1,($3)+(20)*
+1,0,$4,0,0,20*%
+%AMMACPO*
+$4=($2)-($1)*
+$5=($2)-($4)*
+21,1,$1,$5,0,0,20*
+1,1,$4,0,($4)/(2),20*
+1,1,$4,0,($4)/(-2),20*
+1,0,$3,0,0,20*%
+%AMMACLO*
+$4=($1)-($2)*
+$5=($1)-($4)*
+21,1,$5,$2,0,0,20*
+1,1,$4,($4)/(2),0,20*
+1,1,$4,($4)/(-2),0,20*
+1,0,$3,0,0,20*%
+%AMMACR*
+21,1,$1,$2,0,0,20*
+1,0,$3,0,0,20*%
+%ADD10C,0.01*%
+%ADD11C,1X0.4*%
+%ADD12MACR,1X0.5X0.2*%
+%ADD13MACLO,1X0.5X0.2*%
+%ADD14MACPO,0.5X1X0.2*%
+%ADD15MACP,1X5X90X0.2*%
+%ADD16COMP,0*%
+%ADD17COMP,45*%
+%ADD18COMP,-45*%
+G75*
+%LPD*%
+D10*
+G01*
+X49630Y-24751D02*
+X124805Y2611D01*
+G03*
+X130782Y15428I-3420J9397D01*
+G01*
+X103420Y90603D01*
+G03*
+X90603Y96580I-9397J-3420D01*
+G01*
+X15428Y69218D01*
+G03*
+X9451Y56401I3420J-9397D01*
+G01*
+X36813Y-18774D01*
+G03*
+X49630Y-24751I9397J3420D01*
+G01*
+G36*
+G01*
+X79099Y-3383D02*
+X83797Y-1673D01*
+G03*
+X86786Y4735I-1710J4698D01*
+G01*
+X62844Y70514D01*
+G03*
+X56436Y73502I-4698J-1710D01*
+G01*
+X51737Y71792D01*
+G03*
+X48749Y65383I1710J-4698D01*
+G01*
+X72690Y-395D01*
+G03*
+X79099Y-3383I4698J1710D01*
+G01*
+G37*
+D11*
+X60305Y-10224D03*
+D12*
+X53464Y8570D03*
+D13*
+X46624Y27364D03*
+D14*
+X39784Y46158D03*
+D15*
+X32943Y64952D03*
+D16*
+X93609Y44465D03*
+D17*
+X85058Y67957D03*
+D18*
+X102159Y20973D03*
+M02*
diff --git a/tests/expects/RS2724x_save.gtl b/tests/expects/RS2724x_save.gtl
new file mode 100644
index 0000000..02dbaa8
--- /dev/null
+++ b/tests/expects/RS2724x_save.gtl
@@ -0,0 +1,79 @@
+%MOMM*%
+%FSLAX34Y34*%
+%INTop Layer*%
+%IPPOS*%
+%AMCOMP*
+20,1,0.2,0,0.1,0.4,0.1,$1*
+21,1,0.4,0.2,-0.2,-0.1,$1*
+1,1,0.4,-1.2,0,$1*
+4,1,4,1.2,0,1.4,-0.2,1.2,-0.4,1,-0.2,1.2,0,$1*
+5,1,6,1.2,0.2,0.4,$1*
+6,-0.7,0,0.5,0.05,0.15,2,0.05,0.6,$1*
+7,0.7,0,0.6,0.5,0.15,$1*%
+%ADD10C,0.01*%
+%ADD11C,1X0.4*%
+%ADD12R,1X0.5X0.2*%
+%ADD13O,1X0.5X0.2*%
+%ADD14O,0.5X1X0.2*%
+%ADD15P,1X5X90X0.2*%
+%ADD16COMP,0*%
+%ADD17COMP,45*%
+%ADD18COMP,-45*%
+G75*
+%LPD*%
+D10*
+G01*
+X10000Y0D02*
+X90000Y0D01*
+G03*
+X100000Y10000I0J10000D01*
+G01*
+X100000Y90000D01*
+G03*
+X90000Y100000I-10000J0D01*
+G01*
+X10000Y100000D01*
+G03*
+X0Y90000I0J-10000D01*
+G01*
+X0Y10000D01*
+G03*
+X10000Y0I10000J0D01*
+G01*
+G36*
+G01*
+X45000Y10000D02*
+X50000Y10000D01*
+G03*
+X55000Y15000I0J5000D01*
+G01*
+X55000Y85000D01*
+G03*
+X50000Y90000I-5000J0D01*
+G01*
+X45000Y90000D01*
+G03*
+X40000Y85000I0J-5000D01*
+G01*
+X40000Y15000D01*
+G03*
+X45000Y10000I5000J0D01*
+G01*
+G37*
+D11*
+X25000Y10000D03*
+D12*
+X25000Y30000D03*
+D13*
+X25000Y50000D03*
+D14*
+X25000Y70000D03*
+D15*
+X25000Y90000D03*
+D16*
+X75000Y50000D03*
+D17*
+X75000Y75000D03*
+D18*
+X75000Y25000D03*
+M02*
diff --git a/tests/expects/RS2724x_to_inch.gtl b/tests/expects/RS2724x_to_inch.gtl
new file mode 100644
index 0000000..3ec60d8
--- /dev/null
+++ b/tests/expects/RS2724x_to_inch.gtl
@@ -0,0 +1,79 @@
+%MOIN*%
+%FSLAX25Y25*%
+%INTop Layer*%
+%IPPOS*%
+%AMCOMP*
+20,1,0.00787402,0,0.00393701,0.015748,0.00393701,$1*
+21,1,0.015748,0.00787402,-0.00787402,-0.00393701,$1*
+1,1,0.015748,-0.0472441,0,$1*
+4,1,4,0.0472441,0,0.0551181,-0.00787402,0.0472441,-0.015748,0.0393701,-0.00787402,0.0472441,0,$1*
+5,1,6,0.0472441,0.00787402,0.015748,$1*
+6,-0.0275591,0,0.019685,0.0019685,0.00590551,2,0.0019685,0.023622,$1*
+7,0.0275591,0,0.023622,0.019685,0.00590551,$1*%
+%ADD10C,0.0003937*%
+%ADD11C,0.03937X0.01575*%
+%ADD12R,0.03937X0.01969X0.007874*%
+%ADD13O,0.03937X0.01969X0.007874*%
+%ADD14O,0.01969X0.03937X0.007874*%
+%ADD15P,0.03937X5X90X0.007874*%
+%ADD16COMP,0*%
+%ADD17COMP,45*%
+%ADD18COMP,-45*%
+G75*
+%LPD*%
+D10*
+G01*
+X3937Y0D02*
+X35433Y0D01*
+G03*
+X39370Y3937I0J3937D01*
+G01*
+X39370Y35433D01*
+G03*
+X35433Y39370I-3937J0D01*
+G01*
+X3937Y39370D01*
+G03*
+X0Y35433I0J-3937D01*
+G01*
+X0Y3937D01*
+G03*
+X3937Y0I3937J0D01*
+G01*
+G36*
+G01*
+X17717Y3937D02*
+X19685Y3937D01*
+G03*
+X21654Y5906I0J1969D01*
+G01*
+X21654Y33465D01*
+G03*
+X19685Y35433I-1969J0D01*
+G01*
+X17717Y35433D01*
+G03*
+X15748Y33465I0J-1969D01*
+G01*
+X15748Y5906D01*
+G03*
+X17717Y3937I1969J0D01*
+G01*
+G37*
+D11*
+X9843Y3937D03*
+D12*
+X9843Y11811D03*
+D13*
+X9843Y19685D03*
+D14*
+X9843Y27559D03*
+D15*
+X9843Y35433D03*
+D16*
+X29528Y19685D03*
+D17*
+X29528Y29528D03*
+D18*
+X29528Y9843D03*
+M02*
diff --git a/tests/expects/RS2724x_to_metric.gtl b/tests/expects/RS2724x_to_metric.gtl
new file mode 100644
index 0000000..93adfc1
--- /dev/null
+++ b/tests/expects/RS2724x_to_metric.gtl
@@ -0,0 +1,79 @@
+%MOMM*%
+%FSLAX34Y34*%
+%INTop Layer*%
+%IPPOS*%
+%AMCOMP*
+20,1,0.2,0,0.1,0.399999,0.1,$1*
+21,1,0.399999,0.2,-0.2,-0.1,$1*
+1,1,0.399999,-1.2,0,$1*
+4,1,4,1.2,0,1.4,-0.2,1.2,-0.399999,1,-0.2,1.2,0,$1*
+5,1,6,1.2,0.2,0.399999,$1*
+6,-0.700001,0,0.499999,0.0499999,0.15,2,0.0499999,0.599999,$1*
+7,0.700001,0,0.599999,0.499999,0.15,$1*%
+%ADD10C,0.01*%
+%ADD11C,1X0.4*%
+%ADD12R,1X0.5001X0.2*%
+%ADD13O,1X0.5001X0.2*%
+%ADD14O,0.5001X1X0.2*%
+%ADD15P,1X5X90X0.2*%
+%ADD16COMP,0*%
+%ADD17COMP,45*%
+%ADD18COMP,-45*%
+G75*
+%LPD*%
+D10*
+G01*
+X10000Y0D02*
+X90000Y0D01*
+G03*
+X100000Y10000I0J10000D01*
+G01*
+X100000Y90000D01*
+G03*
+X90000Y100000I-10000J0D01*
+G01*
+X10000Y100000D01*
+G03*
+X0Y90000I0J-10000D01*
+G01*
+X0Y10000D01*
+G03*
+X10000Y0I10000J0D01*
+G01*
+G36*
+G01*
+X45001Y10000D02*
+X50000Y10000D01*
+G03*
+X55001Y15001I0J5001D01*
+G01*
+X55001Y85001D01*
+G03*
+X50000Y90000I-5001J0D01*
+G01*
+X45001Y90000D01*
+G03*
+X40000Y85001I0J-5001D01*
+G01*
+X40000Y15001D01*
+G03*
+X45001Y10000I5001J0D01*
+G01*
+G37*
+D11*
+X25001Y10000D03*
+D12*
+X25001Y30000D03*
+D13*
+X25001Y50000D03*
+D14*
+X25001Y70000D03*
+D15*
+X25001Y90000D03*
+D16*
+X75001Y50000D03*
+D17*
+X75001Y75001D03*
+D18*
+X75001Y25001D03*
+M02*
diff --git a/tests/expects/dxf_offset.gtl b/tests/expects/dxf_offset.gtl
new file mode 100644
index 0000000..18d7a62
--- /dev/null
+++ b/tests/expects/dxf_offset.gtl
@@ -0,0 +1,42 @@
+G75*
+%MOMM*%
+%OFA0B0*%
+%FSLAX34Y34*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0*%
+D10*
+G01*
+X200000Y50000D02*
+G75*
+G01*
+X120000Y50000D01*
+G02*
+X110000Y60000I0J10000D01*
+G01*
+X110000Y140000D01*
+G02*
+X120000Y150000I10000J0D01*
+G01*
+X200000Y150000D01*
+G02*
+X210000Y140000I0J-10000D01*
+G01*
+X210000Y60000D01*
+G02*
+X200000Y50000I-10000J0D01*
+G01*
+X119171Y100000D02*
+G75*
+G03*
+X119171Y100000I-3000J0D01*
+G01*
+X119171Y125107D02*
+G75*
+G02*
+X113996Y123041I-3000J0D01*
+G01*
+X116171Y125107D01*
+G01*
+X119171Y125107D01*
+M02*
diff --git a/tests/expects/dxf_rectangle_inch.gtl b/tests/expects/dxf_rectangle_inch.gtl
new file mode 100644
index 0000000..ca99021
--- /dev/null
+++ b/tests/expects/dxf_rectangle_inch.gtl
@@ -0,0 +1,21 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX25Y25*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0*%
+D10*
+G01*
+X0Y0D02*
+X39370Y0D01*
+G01*
+X39370Y0D02*
+X39370Y39370D01*
+G01*
+X39370Y39370D02*
+X0Y39370D01*
+G01*
+X0Y39370D02*
+X0Y0D01*
+M02*
diff --git a/tests/expects/dxf_rectangle_metric.gtl b/tests/expects/dxf_rectangle_metric.gtl
new file mode 100644
index 0000000..db4c439
--- /dev/null
+++ b/tests/expects/dxf_rectangle_metric.gtl
@@ -0,0 +1,21 @@
+G75*
+%MOMM*%
+%OFA0B0*%
+%FSLAX34Y34*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0*%
+D10*
+G01*
+X0Y0D02*
+X100000Y0D01*
+G01*
+X100000Y0D02*
+X100000Y100000D01*
+G01*
+X100000Y100000D02*
+X0Y100000D01*
+G01*
+X0Y100000D02*
+X0Y0D01*
+M02*
diff --git a/tests/expects/dxf_rotate.gtl b/tests/expects/dxf_rotate.gtl
new file mode 100644
index 0000000..912ad60
--- /dev/null
+++ b/tests/expects/dxf_rotate.gtl
@@ -0,0 +1,42 @@
+G75*
+%MOMM*%
+%OFA0B0*%
+%FSLAX34Y34*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0*%
+D10*
+G01*
+X124805Y2611D02*
+G75*
+G01*
+X49630Y-24751D01*
+G02*
+X36813Y-18774I-3420J9397D01*
+G01*
+X9451Y56401D01*
+G02*
+X15428Y69218I9397J3420D01*
+G01*
+X90603Y96580D01*
+G02*
+X103420Y90603I3420J-9397D01*
+G01*
+X130782Y15428D01*
+G02*
+X124805Y2611I-9397J-3420D01*
+G01*
+X31930Y20924D02*
+G75*
+G03*
+X31930Y20924I-3000J0D01*
+G01*
+X23162Y45543D02*
+G75*
+G02*
+X19006Y41831I-2819J-1026D01*
+G01*
+X20343Y44517D01*
+G01*
+X23162Y45543D01*
+M02*
diff --git a/tests/expects/dxf_save_fill.gtl b/tests/expects/dxf_save_fill.gtl
new file mode 100644
index 0000000..c293826
--- /dev/null
+++ b/tests/expects/dxf_save_fill.gtl
@@ -0,0 +1,44 @@
+G75*
+%MOMM*%
+%OFA0B0*%
+%FSLAX34Y34*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0*%
+D10*
+G36*
+G01*
+X90000Y0D02*
+G75*
+G01*
+X10000Y0D01*
+G02*
+X0Y10000I0J10000D01*
+G01*
+X0Y90000D01*
+G02*
+X10000Y100000I10000J0D01*
+G01*
+X90000Y100000D01*
+G02*
+X100000Y90000I0J-10000D01*
+G01*
+X100000Y10000D01*
+G02*
+X90000Y0I-10000J0D01*
+G01*
+X9171Y50000D02*
+G75*
+G03*
+X9171Y50000I-3000J0D01*
+G01*
+X9171Y75107D02*
+G75*
+G02*
+X3996Y73041I-3000J0D01*
+G01*
+X6171Y75107D01*
+G01*
+X9171Y75107D01*
+G37*
+M02*
diff --git a/tests/expects/dxf_save_line.gtl b/tests/expects/dxf_save_line.gtl
new file mode 100644
index 0000000..e9a931b
--- /dev/null
+++ b/tests/expects/dxf_save_line.gtl
@@ -0,0 +1,42 @@
+G75*
+%MOMM*%
+%OFA0B0*%
+%FSLAX34Y34*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0*%
+D10*
+G01*
+X90000Y0D02*
+G75*
+G01*
+X10000Y0D01*
+G02*
+X0Y10000I0J10000D01*
+G01*
+X0Y90000D01*
+G02*
+X10000Y100000I10000J0D01*
+G01*
+X90000Y100000D01*
+G02*
+X100000Y90000I0J-10000D01*
+G01*
+X100000Y10000D01*
+G02*
+X90000Y0I-10000J0D01*
+G01*
+X9171Y50000D02*
+G75*
+G03*
+X9171Y50000I-3000J0D01*
+G01*
+X9171Y75107D02*
+G75*
+G02*
+X3996Y73041I-3000J0D01*
+G01*
+X6171Y75107D01*
+G01*
+X9171Y75107D01*
+M02*
diff --git a/tests/expects/dxf_save_mousebites.gtl b/tests/expects/dxf_save_mousebites.gtl
new file mode 100644
index 0000000..b893454
--- /dev/null
+++ b/tests/expects/dxf_save_mousebites.gtl
@@ -0,0 +1,29 @@
+G75*
+%MOMM*%
+%OFA0B0*%
+%FSLAX34Y34*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0.5*%
+D10*
+X10000Y90000D03*
+X10000Y80000D03*
+X10000Y70000D03*
+X10000Y60000D03*
+X10000Y50000D03*
+X10000Y40000D03*
+X10000Y30000D03*
+X10000Y20000D03*
+X10000Y10000D03*
+
+X50000Y90000D03*
+X50000Y80000D03*
+X50000Y70000D03*
+X50000Y60000D03*
+X50000Y50000D03*
+X50000Y40000D03*
+X50000Y30000D03*
+X50000Y20000D03*
+X50000Y10000D03*
+
+M02*
diff --git a/tests/expects/dxf_save_mousebites.txt b/tests/expects/dxf_save_mousebites.txt
new file mode 100644
index 0000000..4a371ef
--- /dev/null
+++ b/tests/expects/dxf_save_mousebites.txt
@@ -0,0 +1,29 @@
+M48
+FMAT,2
+ICI,OFF
+METRIC,TZ,000.000
+M71
+T01C0.500
+%
+T01
+X1000Y9000
+X1000Y8000
+X1000Y7000
+X1000Y6000
+X1000Y5000
+X1000Y4000
+X1000Y3000
+X1000Y2000
+X1000Y1000
+
+X5000Y9000
+X5000Y8000
+X5000Y7000
+X5000Y6000
+X5000Y5000
+X5000Y4000
+X5000Y3000
+X5000Y2000
+X5000Y1000
+
+M30
diff --git a/tests/expects/dxf_to_inch.gtl b/tests/expects/dxf_to_inch.gtl
new file mode 100644
index 0000000..ee630c9
--- /dev/null
+++ b/tests/expects/dxf_to_inch.gtl
@@ -0,0 +1,42 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX25Y25*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0*%
+D10*
+G01*
+X35433Y0D02*
+G75*
+G01*
+X3937Y0D01*
+G02*
+X0Y3937I0J3937D01*
+G01*
+X0Y35433D01*
+G02*
+X3937Y39370I3937J0D01*
+G01*
+X35433Y39370D01*
+G02*
+X39370Y35433I0J-3937D01*
+G01*
+X39370Y3937D01*
+G02*
+X35433Y0I-3937J0D01*
+G01*
+X3610Y19685D02*
+G75*
+G03*
+X3610Y19685I-1181J0D01*
+G01*
+X3610Y29570D02*
+G75*
+G02*
+X1573Y28756I-1181J0D01*
+G01*
+X2429Y29570D01*
+G01*
+X3610Y29570D01*
+M02*
diff --git a/tests/expects/excellon_offset.txt b/tests/expects/excellon_offset.txt
new file mode 100644
index 0000000..2007248
--- /dev/null
+++ b/tests/expects/excellon_offset.txt
@@ -0,0 +1,43 @@
+M48
+FMAT,2
+ICI,OFF
+METRIC,TZ,000.000
+M71
+T01C0.100
+T02C0.200
+%
+T01
+X16000Y14500
+X16200Y14500
+X16400Y14500
+X16600Y14500
+X16800Y14500
+X17000Y14500
+X17200Y14500
+X17400Y14500
+X17600Y14500
+X17800Y14500
+X18000Y14500
+X18200Y14500
+X18400Y14500
+X18600Y14500
+X18800Y14500
+X19000Y14500
+X19200Y14500
+X19400Y14500
+X19600Y14500
+X19800Y14500
+X20000Y14500
+T02
+X16000Y5500
+X16400Y5500
+X16800Y5500
+X17200Y5500
+X17600Y5500
+X18000Y5500
+X18400Y5500
+X18800Y5500
+X19200Y5500
+X19600Y5500
+X20000Y5500
+M30
diff --git a/tests/expects/excellon_rotate.txt b/tests/expects/excellon_rotate.txt
new file mode 100644
index 0000000..2ef3540
--- /dev/null
+++ b/tests/expects/excellon_rotate.txt
@@ -0,0 +1,43 @@
+M48
+FMAT,2
+ICI,OFF
+METRIC,TZ,000.000
+M71
+T01C0.100
+T02C0.200
+%
+T01
+X5473Y7820
+X5660Y7888
+X5848Y7957
+X6036Y8025
+X6224Y8094
+X6412Y8162
+X6600Y8230
+X6788Y8299
+X6976Y8367
+X7164Y8436
+X7352Y8504
+X7540Y8572
+X7728Y8641
+X7916Y8709
+X8104Y8778
+X8292Y8846
+X8480Y8915
+X8668Y8983
+X8855Y9051
+X9043Y9120
+X9231Y9188
+T02
+X8551Y-637
+X8927Y-500
+X9302Y-364
+X9678Y-227
+X10054Y-90
+X10430Y47
+X10806Y184
+X11182Y320
+X11558Y457
+X11934Y594
+X12309Y731
+M30
diff --git a/tests/expects/excellon_save.txt b/tests/expects/excellon_save.txt
new file mode 100644
index 0000000..a1d2ba8
--- /dev/null
+++ b/tests/expects/excellon_save.txt
@@ -0,0 +1,43 @@
+M48
+FMAT,2
+ICI,OFF
+METRIC,TZ,000.000
+M71
+T01C0.100
+T02C0.200
+%
+T01
+X5000Y9500
+X5200Y9500
+X5400Y9500
+X5600Y9500
+X5800Y9500
+X6000Y9500
+X6200Y9500
+X6400Y9500
+X6600Y9500
+X6800Y9500
+X7000Y9500
+X7200Y9500
+X7400Y9500
+X7600Y9500
+X7800Y9500
+X8000Y9500
+X8200Y9500
+X8400Y9500
+X8600Y9500
+X8800Y9500
+X9000Y9500
+T02
+X5000Y500
+X5400Y500
+X5800Y500
+X6200Y500
+X6600Y500
+X7000Y500
+X7400Y500
+X7800Y500
+X8200Y500
+X8600Y500
+X9000Y500
+M30
diff --git a/tests/expects/excellon_to_inch.txt b/tests/expects/excellon_to_inch.txt
new file mode 100644
index 0000000..8d31df0
--- /dev/null
+++ b/tests/expects/excellon_to_inch.txt
@@ -0,0 +1,43 @@
+M48
+FMAT,2
+ICI,OFF
+INCH,TZ,00.0000
+M72
+T01C0.0039
+T02C0.0078
+%
+T01
+X1969Y3740
+X2047Y3740
+X2126Y3740
+X2205Y3740
+X2283Y3740
+X2362Y3740
+X2441Y3740
+X2520Y3740
+X2598Y3740
+X2677Y3740
+X2756Y3740
+X2835Y3740
+X2913Y3740
+X2992Y3740
+X3071Y3740
+X3150Y3740
+X3228Y3740
+X3307Y3740
+X3386Y3740
+X3465Y3740
+X3543Y3740
+T02
+X1969Y197
+X2126Y197
+X2283Y197
+X2441Y197
+X2598Y197
+X2756Y197
+X2913Y197
+X3071Y197
+X3228Y197
+X3386Y197
+X3543Y197
+M30
diff --git a/tests/expects/excellon_to_metric.txt b/tests/expects/excellon_to_metric.txt
new file mode 100644
index 0000000..a634742
--- /dev/null
+++ b/tests/expects/excellon_to_metric.txt
@@ -0,0 +1,43 @@
+M48
+FMAT,2
+ICI,OFF
+METRIC,TZ,000.000
+M71
+T01C0.099
+T02C0.198
+%
+T01
+X5001Y9500
+X5199Y9500
+X5400Y9500
+X5601Y9500
+X5799Y9500
+X5999Y9500
+X6200Y9500
+X6401Y9500
+X6599Y9500
+X6800Y9500
+X7000Y9500
+X7201Y9500
+X7399Y9500
+X7600Y9500
+X7800Y9500
+X8001Y9500
+X8199Y9500
+X8400Y9500
+X8600Y9500
+X8801Y9500
+X8999Y9500
+T02
+X5001Y500
+X5400Y500
+X5799Y500
+X6200Y500
+X6599Y500
+X7000Y500
+X7399Y500
+X7800Y500
+X8199Y500
+X8600Y500
+X8999Y500
+M30
diff --git a/tests/test_am_expression.py b/tests/test_am_expression.py
new file mode 100644
index 0000000..6786cc3
--- /dev/null
+++ b/tests/test_am_expression.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+import unittest
+from gerberex.am_expression import *
+from gerberex.am_expression import AMOperatorExpression as Op
+from gerber.utils import inch, metric
+from gerber.am_read import read_macro
+
+class TestAMConstantExpression(unittest.TestCase):
+ def setUp(self):
+ self.const_int_value = 7
+ self.const_int = AMConstantExpression(self.const_int_value)
+ self.const_float_value = 1.2345
+ self.const_float = AMConstantExpression(self.const_float_value)
+
+ def test_contruct(self):
+ self.assertEqual(self.const_int.value, self.const_int_value)
+ self.assertEqual(self.const_float.value, self.const_float_value)
+
+ def test_optimize(self):
+ ov = self.const_int.optimize()
+ self.assertEqual(ov.value, self.const_int_value)
+ ov = self.const_float.optimize()
+ self.assertEqual(ov.value, self.const_float_value)
+
+ def test_to_gerber(self):
+ self.assertEqual(self.const_int.to_gerber(), '7')
+ self.assertEqual(self.const_float.to_gerber(), '1.2345')
+
+ def test_to_instructions(self):
+ self.const_int.to_instructions()
+ self.const_float.to_instructions()
+
+class TestAMVariableExpression(unittest.TestCase):
+ def setUp(self):
+ self.var1_num = 1
+ self.var1 = AMVariableExpression(self.var1_num)
+ self.var2_num = 512
+ self.var2 = AMVariableExpression(self.var2_num)
+
+ def test_construction(self):
+ self.assertEqual(self.var1.number, self.var1_num)
+ self.assertEqual(self.var2.number, self.var2_num)
+
+ def test_optimize(self):
+ ov = self.var1.optimize()
+ self.assertTrue(isinstance(ov, AMVariableExpression))
+ self.assertEqual(ov.number, self.var1_num)
+ ov = self.var2.optimize()
+ self.assertTrue(isinstance(ov, AMVariableExpression))
+ self.assertEqual(ov.number, self.var2_num)
+
+ def test_to_gerber(self):
+ self.assertEqual(self.var1.to_gerber(), '$1')
+ self.assertEqual(self.var2.to_gerber(), '$512')
+
+ def test_to_instructions(self):
+ self.var1.to_instructions()
+ self.var2.to_instructions()
+
+class TestAMOperatorExpression(unittest.TestCase):
+ def setUp(self):
+ self.c1 = 10
+ self.c2 = 20
+ self.v1 = 5
+ self.v2 = 9
+ c1 = AMConstantExpression(self.c1)
+ c2 = AMConstantExpression(self.c2)
+ v1 = AMVariableExpression(self.v1)
+ v2 = AMVariableExpression(self.v2)
+
+ self.cc_exps = [
+ (Op.ADD, AMOperatorExpression(Op.ADD, c1, c2)),
+ (Op.SUB, AMOperatorExpression(Op.SUB, c1, c2)),
+ (Op.MUL, AMOperatorExpression(Op.MUL, c1, c2)),
+ (Op.DIV, AMOperatorExpression(Op.DIV, c1, c2)),
+ ]
+
+ self.cv_exps = [
+ (Op.ADD, AMOperatorExpression(Op.ADD, c1, v2)),
+ (Op.SUB, AMOperatorExpression(Op.SUB, c1, v2)),
+ (Op.MUL, AMOperatorExpression(Op.MUL, c1, v2)),
+ (Op.DIV, AMOperatorExpression(Op.DIV, c1, v2)),
+ ]
+ self.vc_exps = [
+ (Op.ADD, AMOperatorExpression(Op.ADD, v1, c2)),
+ (Op.SUB, AMOperatorExpression(Op.SUB, v1, c2)),
+ (Op.MUL, AMOperatorExpression(Op.MUL, v1, c2)),
+ (Op.DIV, AMOperatorExpression(Op.DIV, v1, c2)),
+ ]
+
+ self.composition = AMOperatorExpression(Op.ADD,
+ self.cc_exps[0][1], self.cc_exps[0][1])
+
+ def test_optimize(self):
+ self.assertEqual(self.cc_exps[0][1].optimize().value, self.c1 + self.c2)
+ self.assertEqual(self.cc_exps[1][1].optimize().value, self.c1 - self.c2)
+ self.assertEqual(self.cc_exps[2][1].optimize().value, self.c1 * self.c2)
+ self.assertEqual(self.cc_exps[3][1].optimize().value, self.c1 / self.c2)
+
+ for op, expression in self.cv_exps:
+ o = expression.optimize()
+ self.assertTrue(isinstance(o, AMOperatorExpression))
+ self.assertEqual(o.op, op)
+ self.assertEqual(o.lvalue.value, self.c1)
+ self.assertEqual(o.rvalue.number, self.v2)
+
+ for op, expression in self.vc_exps:
+ o = expression.optimize()
+ self.assertTrue(isinstance(o, AMOperatorExpression))
+ self.assertEqual(o.op, op)
+ self.assertEqual(o.lvalue.number, self.v1)
+ self.assertEqual(o.rvalue.value, self.c2)
+
+ self.assertEqual(self.composition.optimize().value, (self.c1 + self.c2) * 2)
+
+ def test_to_gerber(self):
+ for op, expression in self.cc_exps:
+ self.assertEqual(expression.to_gerber(),
+ '({0}){1}({2})'.format(self.c1, op, self.c2))
+ for op, expression in self.cv_exps:
+ self.assertEqual(expression.to_gerber(),
+ '({0}){1}(${2})'.format(self.c1, op, self.v2))
+ for op, expression in self.vc_exps:
+ self.assertEqual(expression.to_gerber(),
+ '(${0}){1}({2})'.format(self.v1, op, self.c2))
+ self.assertEqual(self.composition.to_gerber(),
+ '(({0})+({1}))+(({2})+({3}))'.format(
+ self.c1, self.c2, self.c1, self.c2
+ ))
+
+ def test_to_instructions(self):
+ for of, expression in self.vc_exps + self.cv_exps + self.cc_exps:
+ expression.to_instructions()
+ self.composition.to_instructions()
+
+class TestAMExpression(unittest.TestCase):
+ def setUp(self):
+ self.c1 = 10
+ self.c1_exp = AMConstantExpression(self.c1)
+ self.v1 = 5
+ self.v1_exp = AMVariableExpression(self.v1)
+ self.op_exp = AMOperatorExpression(Op.ADD, self.c1_exp, self.v1_exp)
+
+ def test_to_inch(self):
+ o = self.c1_exp.to_inch().optimize()
+ self.assertEqual(o.value, inch(self.c1))
+ o = self.v1_exp.to_inch().optimize()
+ self.assertTrue(isinstance(o, AMOperatorExpression))
+ self.assertEqual(o.op, Op.DIV)
+ o = self.op_exp.to_inch().optimize()
+ self.assertTrue(isinstance(o, AMOperatorExpression))
+ self.assertEqual(o.op, Op.DIV)
+
+ def test_to_metric(self):
+ o = self.c1_exp.to_metric().optimize()
+ self.assertEqual(o.value, metric(self.c1))
+ o = self.v1_exp.to_metric().optimize()
+ self.assertTrue(isinstance(o, AMOperatorExpression))
+ self.assertEqual(o.op, Op.MUL)
+ o = self.op_exp.to_metric().optimize()
+ self.assertTrue(isinstance(o, AMOperatorExpression))
+ self.assertEqual(o.op, Op.MUL)
+
+class TestEvalMacro(unittest.TestCase):
+ def test_eval_macro(self):
+ macros = [
+ '$1=5.5*',
+ '$2=$3*',
+ '$3=(1.23)+(4.56)*',
+ '$3=(1.23)-(4.56)*',
+ '$3=(1.23)X(4.56)*',
+ '$3=(1.23)/(4.56)*',
+ '$3=(10.2)X($2)*',
+ '1,1.2*',
+ '1,$2*',
+ '1,($2)+($3)*',
+ #'1,(2.0)-($3)*', # This doesn't pass due to pcb-tools bug
+ '1,($2)X($3)*',
+ '1,($2)/($3)*',
+ '1,2.1,3.2*2,(3.1)/($1),$2*'
+ ]
+ for macro in macros:
+ self._eval_macro_string(macro)
+
+ def _eval_macro_string(self, macro):
+ expressions = eval_macro(read_macro(macro))
+ gerber = self._to_gerber(expressions)
+ self.assertEqual(macro, gerber)
+
+ def _to_gerber(self, expressions_list):
+ gerber = ''
+ for number, expressions in expressions_list:
+ self.assertTrue(isinstance(number, int))
+ if number > 0:
+ egerbers = [exp.to_gerber() for exp in expressions]
+ gerber += '{0},{1}*'.format(number, ','.join(egerbers))
+ else:
+ self.assertEqual(len(expressions), 1)
+ gerber += '${0}={1}*'.format(-number, expressions[0].to_gerber())
+ return gerber
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_dxf.py b/tests/test_dxf.py
new file mode 100644
index 0000000..31589da
--- /dev/null
+++ b/tests/test_dxf.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+import os
+import unittest
+import gerberex
+from gerber.utils import inch, metric
+
+
+class TestExcellon(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ os.chdir(os.path.dirname(__file__))
+ cls.INDIR = 'data'
+ cls.OUTDIR = 'outputs'
+ cls.EXPECTSDIR = 'expects'
+ cls.OUTPREFIX = 'dxf_'
+ cls.METRIC_FILE = os.path.join(cls.INDIR, 'ref_dxf_metric.dxf')
+ cls.INCH_FILE = os.path.join(cls.INDIR, 'ref_dxf_inch.dxf')
+ cls.MOUSEBITES_FILE = os.path.join(cls.INDIR, 'ref_dxf_mousebites.dxf')
+ try:
+ os.mkdir(cls.OUTDIR)
+ except FileExistsError:
+ pass
+
+ def _checkResult(self, file):
+ with open(file, 'r') as f:
+ data = f.read()
+ with open(os.path.join(self.EXPECTSDIR, os.path.basename(file)), 'r') as f:
+ expect = f.read()
+ self.assertEqual(data, expect)
+
+ def test_save_line(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'save_line.gtl')
+ dxf = gerberex.read(self.METRIC_FILE)
+ dxf.draw_mode = dxf.DM_LINE
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def test_save_fill(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'save_fill.gtl')
+ dxf = gerberex.read(self.METRIC_FILE)
+ dxf.draw_mode = dxf.DM_FILL
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def test_save_mousebites(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'save_mousebites.gtl')
+ dxf = gerberex.read(self.MOUSEBITES_FILE)
+ dxf.draw_mode = dxf.DM_MOUSE_BITES
+ dxf.width = 0.5
+ dxf.pitch = 1
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def test_save_excellon(self):
+ outfile = os.path.join(
+ self.OUTDIR, self.OUTPREFIX + 'save_mousebites.txt')
+ dxf = gerberex.read(self.MOUSEBITES_FILE)
+ dxf.draw_mode = dxf.DM_MOUSE_BITES
+ dxf.format = (3,3)
+ dxf.width = 0.5
+ dxf.pitch = 1
+ dxf.write(outfile, filetype=dxf.FT_EXCELLON)
+ self._checkResult(outfile)
+
+ def test_to_inch(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'to_inch.gtl')
+ dxf = gerberex.read(self.METRIC_FILE)
+ dxf.to_inch()
+ dxf.format = (2, 5)
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def _test_to_metric(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'to_metric.gtl')
+ dxf = gerberex.read(self.INCH_FILE)
+ dxf.to_metric()
+ dxf.format = (3, 5)
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def test_offset(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'offset.gtl')
+ dxf = gerberex.read(self.METRIC_FILE)
+ dxf.offset(11, 5)
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def test_rotate(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'rotate.gtl')
+ dxf = gerberex.read(self.METRIC_FILE)
+ dxf.rotate(20, (10, 10))
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def test_rectangle_metric(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'rectangle_metric.gtl')
+ dxf = gerberex.DxfFile.rectangle(width=10, height=10, units='metric')
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+ def test_rectangle_inch(self):
+ outfile = os.path.join(
+ self.OUTDIR, self.OUTPREFIX + 'rectangle_inch.gtl')
+ dxf = gerberex.DxfFile.rectangle(width=inch(10), height=inch(10), units='inch')
+ dxf.write(outfile)
+ self._checkResult(outfile)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_excellon.py b/tests/test_excellon.py
new file mode 100644
index 0000000..36093cd
--- /dev/null
+++ b/tests/test_excellon.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+import os
+import unittest
+import gerberex
+
+
+class TestExcellon(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ os.chdir(os.path.dirname(__file__))
+ cls.INDIR = 'data'
+ cls.OUTDIR = 'outputs'
+ cls.EXPECTSDIR = 'expects'
+ cls.OUTPREFIX = 'excellon_'
+ cls.METRIC_FILE = os.path.join(cls.INDIR, 'ref_drill_metric.txt')
+ cls.INCH_FILE = os.path.join(cls.INDIR, 'ref_drill_inch.txt')
+ try:
+ os.mkdir(cls.OUTDIR)
+ except FileExistsError:
+ pass
+
+ def _checkResult(self, file):
+ with open(file, 'r') as f:
+ data = f.read()
+ with open(os.path.join(self.EXPECTSDIR, os.path.basename(file)), 'r') as f:
+ expect = f.read()
+ self.assertEqual(data, expect)
+ pass
+
+ def test_save(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'save.txt')
+ drill = gerberex.read(self.METRIC_FILE)
+ drill.write(outfile)
+ self._checkResult(outfile)
+
+ def test_to_inch(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'to_inch.txt')
+ drill = gerberex.read(self.METRIC_FILE)
+ drill.to_inch()
+ drill.format = (2, 4)
+ drill.write(outfile)
+ self._checkResult(outfile)
+
+ def test_to_metric(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'to_metric.txt')
+ drill = gerberex.read(self.INCH_FILE)
+ drill.to_metric()
+ drill.format = (3, 3)
+ drill.write(outfile)
+ self._checkResult(outfile)
+
+ def test_offset(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'offset.txt')
+ drill = gerberex.read(self.METRIC_FILE)
+ drill.offset(11, 5)
+ drill.write(outfile)
+ self._checkResult(outfile)
+
+ def test_rotate(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'rotate.txt')
+ drill = gerberex.read(self.METRIC_FILE)
+ drill.rotate(20, (10, 10))
+ drill.write(outfile)
+ self._checkResult(outfile)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_rs274x.py b/tests/test_rs274x.py
new file mode 100644
index 0000000..3e44066
--- /dev/null
+++ b/tests/test_rs274x.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+import os
+import unittest
+import gerberex
+
+class TestRs274x(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ os.chdir(os.path.dirname(__file__))
+ cls.INDIR = 'data'
+ cls.OUTDIR = 'outputs'
+ cls.EXPECTSDIR = 'expects'
+ cls.OUTPREFIX = 'RS2724x_'
+ cls.METRIC_FILE = os.path.join(cls.INDIR, 'ref_gerber_metric.gtl')
+ cls.INCH_FILE = os.path.join(cls.INDIR, 'ref_gerber_inch.gtl')
+ try:
+ os.mkdir(cls.OUTDIR)
+ except FileExistsError:
+ pass
+
+ def _checkResult(self, file):
+ with open(file, 'r') as f:
+ data = f.read()
+ with open(os.path.join(self.EXPECTSDIR, os.path.basename(file)), 'r') as f:
+ expect = f.read()
+ self.assertEqual(data, expect)
+
+ def test_save(self):
+ outfile=os.path.join(self.OUTDIR, self.OUTPREFIX + 'save.gtl')
+ gerber = gerberex.read(self.METRIC_FILE)
+ gerber.write(outfile)
+ self._checkResult(outfile)
+
+ def test_to_inch(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'to_inch.gtl')
+ gerber = gerberex.read(self.METRIC_FILE)
+ gerber.to_inch()
+ gerber.format = (2,5)
+ gerber.write(outfile)
+ self._checkResult(outfile)
+
+ def test_to_metric(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'to_metric.gtl')
+ gerber = gerberex.read(self.INCH_FILE)
+ gerber.to_metric()
+ gerber.format = (3, 4)
+ gerber.write(outfile)
+ self._checkResult(outfile)
+
+ def test_offset(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'offset.gtl')
+ gerber = gerberex.read(self.METRIC_FILE)
+ gerber.offset(11, 5)
+ gerber.write(outfile)
+ self._checkResult(outfile)
+
+ def test_rotate(self):
+ outfile = os.path.join(self.OUTDIR, self.OUTPREFIX + 'rotate.gtl')
+ gerber = gerberex.read(self.METRIC_FILE)
+ gerber.rotate(20, (10,10))
+ gerber.write(outfile)
+ self._checkResult(outfile)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_utility.py b/tests/test_utility.py
new file mode 100644
index 0000000..4ede8d5
--- /dev/null
+++ b/tests/test_utility.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+import unittest
+
+from gerberex.utility import *
+from math import sqrt
+
+class TestUtility(unittest.TestCase):
+ def test_rotate(self):
+ x0, y0 = (10, 0)
+
+ x1, y1 = rotate(x0, y0, 60, (0, 0))
+ self.assertAlmostEqual(x1, 5)
+ self.assertAlmostEqual(y1, 10 * sqrt(3) / 2)
+
+ x1, y1 = rotate(x0, y0, 180, (5, 0))
+ self.assertAlmostEqual(x1, 0)
+ self.assertAlmostEqual(y1, 0)
+
+ x1, y1 = rotate(x0, y0, -90, (10, 5))
+ self.assertAlmostEqual(x1, 5)
+ self.assertAlmostEqual(y1, 5)
+
+ def test_is_equal_value(self):
+ a = 10.0001
+ b = 10.01
+
+ self.assertTrue(is_equal_value(a, b, 0.1))
+ self.assertTrue(is_equal_value(a, b, 0.01))
+ self.assertFalse(is_equal_value(a, b, 0.001))
+ self.assertFalse(is_equal_value(a, b))
+
+ self.assertTrue(is_equal_value(b, a, 0.1))
+ self.assertTrue(is_equal_value(b, a, 0.01))
+ self.assertFalse(is_equal_value(b, a, 0.001))
+ self.assertFalse(is_equal_value(b, a))
+
+ def test_is_equal_point(self):
+ p0 = (10.01, 5.001)
+ p1 = (10.0001, 5)
+ self.assertTrue(is_equal_point(p0, p1, 0.1))
+ self.assertTrue(is_equal_point(p0, p1, 0.01))
+ self.assertFalse(is_equal_point(p0, p1, 0.001))
+ self.assertFalse(is_equal_point(p0, p1))
+ self.assertTrue(is_equal_point(p1, p0, 0.1))
+ self.assertTrue(is_equal_point(p1, p0, 0.01))
+ self.assertFalse(is_equal_point(p1, p0, 0.001))
+ self.assertFalse(is_equal_point(p1, p0))
+
+ p0 = (5.001, 10.01)
+ p1 = (5, 10.0001)
+ self.assertTrue(is_equal_point(p0, p1, 0.1))
+ self.assertTrue(is_equal_point(p0, p1, 0.01))
+ self.assertFalse(is_equal_point(p0, p1, 0.001))
+ self.assertFalse(is_equal_point(p0, p1))
+ self.assertTrue(is_equal_point(p1, p0, 0.1))
+ self.assertTrue(is_equal_point(p1, p0, 0.01))
+ self.assertFalse(is_equal_point(p1, p0, 0.001))
+ self.assertFalse(is_equal_point(p1, p0))
+
+if __name__ == '__main__':
+ unittest.main()