summaryrefslogtreecommitdiff
path: root/gerber
diff options
context:
space:
mode:
authorHamilton Kibbe <ham@hamiltonkib.be>2014-10-13 12:38:57 -0400
committerHamilton Kibbe <ham@hamiltonkib.be>2014-10-13 12:38:57 -0400
commit6adcdbae5fc06959203761616e778ba4594475cc (patch)
tree1b878d7fe63645899a0d631849596b70bd002b07 /gerber
parent152fca07685d6f96f5e5bad723f1f62de99d8b7d (diff)
parent8c5c7ec8bbc8a074884ef04b566f9c0ecd6e78bb (diff)
downloadgerbonara-6adcdbae5fc06959203761616e778ba4594475cc.tar.gz
gerbonara-6adcdbae5fc06959203761616e778ba4594475cc.tar.bz2
gerbonara-6adcdbae5fc06959203761616e778ba4594475cc.zip
Merge branch 'master' of https://github.com/hamiltonkibbe/gerber-tools
Diffstat (limited to 'gerber')
-rw-r--r--gerber/cam.py (renamed from gerber/cnc.py)17
-rw-r--r--gerber/common.py4
-rwxr-xr-xgerber/excellon.py170
-rw-r--r--gerber/gerber_statements.py23
-rw-r--r--gerber/render/render.py42
-rw-r--r--gerber/render/svgwrite_backend.py29
-rw-r--r--gerber/rs274x.py (renamed from gerber/gerber.py)56
-rw-r--r--gerber/tests/test_cam.py (renamed from gerber/tests/test_cnc.py)6
-rw-r--r--gerber/tests/test_gerber_statements.py76
9 files changed, 348 insertions, 75 deletions
diff --git a/gerber/cnc.py b/gerber/cam.py
index d17517a..e7a49d1 100644
--- a/gerber/cnc.py
+++ b/gerber/cam.py
@@ -15,16 +15,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""
-gerber.cnc
+CAM File
============
-**CNC file classes**
+**AM file classes**
This module provides common base classes for Excellon/Gerber CNC files
"""
class FileSettings(object):
- """ CNC File Settings
+ """ CAM File Settings
Provides a common representation of gerber/excellon file settings
"""
@@ -60,7 +60,7 @@ class FileSettings(object):
raise KeyError()
-class CncFile(object):
+class CamFile(object):
""" Base class for Gerber/Excellon files.
Provides a common set of settings parameters.
@@ -71,7 +71,10 @@ class CncFile(object):
The current file configuration.
filename : string
- Name of the file that this CncFile represents.
+ Name of the file that this CamFile represents.
+
+ layer_name : string
+ Name of the PCB layer that the file represents
Attributes
----------
@@ -92,7 +95,8 @@ class CncFile(object):
decimal digits)
"""
- def __init__(self, statements=None, settings=None, filename=None):
+ def __init__(self, statements=None, settings=None, filename=None,
+ layer_name=None):
if settings is not None:
self.notation = settings['notation']
self.units = settings['units']
@@ -105,6 +109,7 @@ class CncFile(object):
self.format = (2, 5)
self.statements = statements if statements is not None else []
self.filename = filename
+ self.layer_name = layer_name
@property
def settings(self):
diff --git a/gerber/common.py b/gerber/common.py
index 0092ec8..6e8c862 100644
--- a/gerber/common.py
+++ b/gerber/common.py
@@ -30,12 +30,12 @@ def read(filename):
CncFile object representing the file, either GerberFile or
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
"""
- import gerber
+ import rs274x
import excellon
from utils import detect_file_format
fmt = detect_file_format(filename)
if fmt == 'rs274x':
- return gerber.read(filename)
+ return rs274x.read(filename)
elif fmt == 'excellon':
return excellon.read(filename)
else:
diff --git a/gerber/excellon.py b/gerber/excellon.py
index 4166de6..13aacc6 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -24,16 +24,32 @@ This module provides Excellon file classes and parsing utilities
from .excellon_statements import *
-from .cnc import CncFile, FileSettings
+from .cam import CamFile, FileSettings
+import math
def read(filename):
""" Read data from filename and return an ExcellonFile
+ Parameters
+ ----------
+ filename : string
+ Filename of file to parse
+
+ Returns
+ -------
+ file : :class:`gerber.excellon.ExcellonFile`
+ An ExcellonFile created from the specified file.
+
"""
- return ExcellonParser().parse(filename)
+ detected_settings = detect_excellon_format(filename)
+ settings = FileSettings(**detected_settings)
+ zeros = ''
+ print('Detected %d:%d format with %s zero suppression' %
+ (settings.format[0], settings.format[1], settings.zero_suppression))
+ return ExcellonParser(settings).parse(filename)
-class ExcellonFile(CncFile):
+class ExcellonFile(CamFile):
""" A class representing a single excellon file
The ExcellonFile class represents a single excellon file.
@@ -69,6 +85,14 @@ class ExcellonFile(CncFile):
def render(self, ctx, filename=None):
""" Generate image of file
+
+ Parameters
+ ----------
+ ctx : :class:`gerber.render.GerberContext`
+ GerberContext subclass used for rendering the image
+
+ filename : string <optional>
+ If provided, the rendered image will be saved to `filename`
"""
for tool, pos in self.hits:
ctx.drill(pos[0], pos[1], tool.diameter)
@@ -83,8 +107,13 @@ class ExcellonFile(CncFile):
class ExcellonParser(object):
""" Excellon File Parser
+
+ Parameters
+ ----------
+ settings : FileSettings or dict-like
+ Excellon file settings to use when interpreting the excellon file.
"""
- def __init__(self):
+ def __init__(self, settings=None):
self.notation = 'absolute'
self.units = 'inch'
self.zero_suppression = 'trailing'
@@ -95,6 +124,37 @@ class ExcellonParser(object):
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
+ if settings is not None:
+ self.units = settings['units']
+ self.zero_suppression = settings['zero_suppression']
+ self.notation = settings['notation']
+ self.format = settings['format']
+
+
+ @property
+ def coordinates(self):
+ return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
+
+ @property
+ def bounds(self):
+ xmin = ymin = 100000000000
+ xmax = ymax = -100000000000
+ for x, y in self.coordinates:
+ if x is not None:
+ xmin = x if x < xmin else xmin
+ xmax = x if x > xmax else xmax
+ if y is not None:
+ ymin = y if y < ymin else ymin
+ ymax = y if y > ymax else ymax
+ return ((xmin, xmax), (ymin, ymax))
+
+ @property
+ def hole_sizes(self):
+ return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)]
+
+ @property
+ def hole_count(self):
+ return len(self.hits)
def parse(self, filename):
with open(filename, 'r') as f:
@@ -194,3 +254,105 @@ class ExcellonParser(object):
return FileSettings(units=self.units, format=self.format,
zero_suppression=self.zero_suppression,
notation=self.notation)
+
+
+def detect_excellon_format(filename):
+ """ Detect excellon file decimal format and zero-suppression settings.
+
+ Parameters
+ ----------
+ filename : string
+ Name of the file to parse. This does not check if the file is actually
+ an Excellon file, so do that before calling this.
+
+ Returns
+ -------
+ settings : dict
+ Detected excellon file settings. Keys are
+ - `format`: decimal format as tuple (<int part>, <decimal part>)
+ - `zero_suppression`: zero suppression, 'leading' or 'trailing'
+ """
+ results = {}
+ detected_zeros = None
+ detected_format = None
+ zs_options = ('leading', 'trailing', )
+ format_options = ((2, 4), (2, 5), (3, 3),)
+
+ # Check for obvious clues:
+ p = ExcellonParser()
+ p.parse(filename)
+
+ # Get zero_suppression from a unit statement
+ zero_statements = [stmt.zero_suppression for stmt in p.statements
+ if isinstance(stmt, UnitStmt)]
+
+ # get format from altium comment
+ format_comment = [stmt.comment for stmt in p.statements
+ if isinstance(stmt, CommentStmt)
+ and 'FILE_FORMAT' in stmt.comment]
+
+ detected_format = (tuple([int(val) for val in
+ format_comment[0].split('=')[1].split(':')])
+ if len(format_comment) == 1 else None)
+ detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None
+
+ # Bail out here if possible
+ if detected_format is not None and detected_zeros is not None:
+ return {'format': detected_format, 'zero_suppression': detected_zeros}
+
+ # Only look at remaining options
+ if detected_format is not None:
+ format_options = (detected_format,)
+ if detected_zeros is not None:
+ zs_options = (detected_zeros,)
+
+ # Brute force all remaining options, and pick the best looking one...
+ for zs in zs_options:
+ for fmt in format_options:
+ key = (fmt, zs)
+ settings = FileSettings(zero_suppression=zs, format=fmt)
+ try:
+ p = ExcellonParser(settings)
+ p.parse(filename)
+ size = tuple([t[1] - t[0] for t in p.bounds])
+ hole_area = 0.0
+ for hit in p.hits:
+ tool = hit[0]
+ hole_area += math.pow(math.pi * tool.diameter / 2., 2)
+ results[key] = (size, p.hole_count, hole_area)
+ except:
+ pass
+
+ # See if any of the dimensions are left with only a single option
+ formats = set(key[0] for key in results.iterkeys())
+ zeros = set(key[1] for key in results.iterkeys())
+ if len(formats) == 1:
+ detected_format = formats.pop()
+ if len(zeros) == 1:
+ detected_zeros = zeros.pop()
+
+ # Bail out here if we got everything....
+ if detected_format is not None and detected_zeros is not None:
+ return {'format': detected_format, 'zero_suppression': detected_zeros}
+
+ # Otherwise score each option and pick the best candidate
+ else:
+ scores = {}
+ for key in results.keys():
+ size, count, diameter = results[key]
+ scores[key] = _layer_size_score(size, count, diameter)
+ minscore = min(scores.values())
+ for key in scores.iterkeys():
+ if scores[key] == minscore:
+ return {'format': key[0], 'zero_suppression': key[1]}
+
+
+def _layer_size_score(size, hole_count, hole_area):
+ """ Heuristic used for determining the correct file number interpretation.
+ Lower is better.
+ """
+ board_area = size[0] * size[1]
+ hole_percentage = hole_area / board_area
+ hole_score = (hole_percentage - 0.25) ** 2
+ size_score = (board_area - 8) **2
+ return hole_score * size_score
diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py
index 9072b58..218074f 100644
--- a/gerber/gerber_statements.py
+++ b/gerber/gerber_statements.py
@@ -17,9 +17,9 @@ __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt',
class Statement(object):
""" Gerber statement Base class
-
+
The statement class provides a type attribute.
-
+
Parameters
----------
type : string
@@ -27,7 +27,7 @@ class Statement(object):
Attributes
----------
- type : string
+ type : string
String identifying the statement type.
"""
def __init__(self, stype):
@@ -45,9 +45,9 @@ class Statement(object):
class ParamStmt(Statement):
""" Gerber parameter statement Base class
-
+
The parameter statement class provides a parameter type attribute.
-
+
Parameters
----------
param : string
@@ -55,7 +55,7 @@ class ParamStmt(Statement):
Attributes
----------
- param : string
+ param : string
Parameter type code
"""
def __init__(self, param):
@@ -133,7 +133,12 @@ class MOParamStmt(ParamStmt):
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('param')
- mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric'
+ if stmt_dict.get('mo').lower() == 'in':
+ mo = 'inch'
+ elif stmt_dict.get('mo').lower() == 'mm':
+ mo = 'metric'
+ else:
+ mo = None
return cls(param, mo)
def __init__(self, param, mo):
@@ -260,7 +265,7 @@ class LPParamStmt(ParamStmt):
@classmethod
def from_dict(cls, stmt_dict):
- param = stmt_dict.get('lp')
+ param = stmt_dict['param']
lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark'
return cls(param, lp)
@@ -667,6 +672,6 @@ class UnknownStmt(Statement):
def __init__(self, line):
Statement.__init__(self, "UNKNOWN")
self.line = line
-
+
def to_gerber(self):
return self.line
diff --git a/gerber/render/render.py b/gerber/render/render.py
index e76aed1..f7e4485 100644
--- a/gerber/render/render.py
+++ b/gerber/render/render.py
@@ -83,6 +83,9 @@ class GerberContext(object):
background_color : tuple (<float>, <float>, <float>)
Color of the background. Used when exposing areas in 'clear' level
polarity mode. Format is the same as for `color`.
+
+ alpha : float
+ Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.)
"""
def __init__(self):
self.settings = {}
@@ -96,11 +99,12 @@ class GerberContext(object):
self.level_polarity = 'dark'
self.region_mode = 'off'
self.quadrant_mode = 'multi-quadrant'
-
+ self.step_and_repeat = (1, 1, 0, 0)
self.color = (0.7215, 0.451, 0.200)
self.drill_color = (0.25, 0.25, 0.25)
self.background_color = (0.0, 0.0, 0.0)
-
+ self.alpha = 1.0
+
def set_format(self, settings):
""" Set source file format.
@@ -260,6 +264,19 @@ class GerberContext(object):
"""
self.background_color = color
+ def set_alpha(self, alpha):
+ """ Set layer rendering opacity
+
+ .. note::
+ Not all backends/rendering devices support this parameter.
+
+ Parameters
+ ----------
+ alpha : float
+ Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque)
+ """
+ self.alpha = alpha
+
def resolve(self, x, y):
""" Resolve missing x or y coordinates in a coordinate command.
@@ -415,6 +432,12 @@ class GerberContext(object):
"""
pass
+ def region_contour(self, x, y):
+ pass
+
+ def fill_region(self):
+ pass
+
def evaluate(self, stmt):
""" Evaluate Gerber statement and update image accordingly.
@@ -450,7 +473,7 @@ class GerberContext(object):
def _evaluate_mode(self, stmt):
if stmt.type == 'RegionMode':
if self.region_mode == 'on' and stmt.mode == 'off':
- self._fill_region()
+ self.fill_region()
self.region_mode = stmt.mode
elif stmt.type == 'QuadrantMode':
self.quadrant_mode = stmt.mode
@@ -460,11 +483,11 @@ class GerberContext(object):
self.set_coord_format(stmt.zero_suppression, stmt.format,
stmt.notation)
self.set_coord_notation(stmt.notation)
- elif stmt.param == "MO:":
+ elif stmt.param == "MO":
self.set_coord_unit(stmt.mode)
- elif stmt.param == "IP:":
+ elif stmt.param == "IP":
self.set_image_polarity(stmt.ip)
- elif stmt.param == "LP:":
+ elif stmt.param == "LP":
self.set_level_polarity(stmt.lp)
elif stmt.param == "AD":
self.define_aperture(stmt.d, stmt.shape, stmt.modifiers)
@@ -477,7 +500,10 @@ class GerberContext(object):
self.direction = ('clockwise' if stmt.function in ('G02', 'G2')
else 'counterclockwise')
if stmt.op == "D01":
- self.stroke(stmt.x, stmt.y, stmt.i, stmt.j)
+ if self.region_mode == 'on':
+ self.region_contour(stmt.x, stmt.y)
+ else:
+ self.stroke(stmt.x, stmt.y, stmt.i, stmt.j)
elif stmt.op == "D02":
self.move(stmt.x, stmt.y)
elif stmt.op == "D03":
@@ -486,5 +512,3 @@ class GerberContext(object):
def _evaluate_aperture(self, stmt):
self.set_aperture(stmt.d)
- def _fill_region(self):
- pass
diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py
index 7570c84..78961da 100644
--- a/gerber/render/svgwrite_backend.py
+++ b/gerber/render/svgwrite_backend.py
@@ -117,17 +117,25 @@ class GerberSvgContext(GerberContext):
self.apertures = {}
self.dwg = svgwrite.Drawing()
+ self.dwg.transform = 'scale 1 -1'
self.background = False
+ self.region_path = None
def set_bounds(self, bounds):
xbounds, ybounds = bounds
size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0]))
if not self.background:
+ self.dwg = svgwrite.Drawing(viewBox='%f, %f, %f, %f' % (SCALE*xbounds[0], -SCALE*ybounds[1],size[0], size[1]))
self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0],
-SCALE * ybounds[1]),
- size=size, fill="black"))
+ size=size, fill=convert_color(self.background_color)))
self.background = True
+ def set_alpha(self, alpha):
+ super(GerberSvgContext, self).set_alpha(alpha)
+ import warnings
+ warnings.warn('SVG output does not support transparency')
+
def define_aperture(self, d, shape, modifiers):
aperture = None
if shape == 'C':
@@ -173,7 +181,8 @@ class GerberSvgContext(GerberContext):
ap = self.apertures.get(self.aperture, None)
if ap is None:
return
- color = (convert_color(self.color) if self.level_polarity == 'dark'
+
+ color = (convert_color(self.color) if self.level_polarity == 'dark'
else convert_color(self.background_color))
for shape in ap.flash(self, x, y, color):
self.dwg.add(shape)
@@ -185,5 +194,21 @@ class GerberSvgContext(GerberContext):
fill=convert_color(self.drill_color))
self.dwg.add(hit)
+ def region_contour(self, x, y):
+ super(GerberSvgContext, self).region_contour(x, y)
+ x, y = self.resolve(x, y)
+ color = (convert_color(self.color) if self.level_polarity == 'dark'
+ else convert_color(self.background_color))
+ if self.region_path is None:
+ self.region_path = self.dwg.path(d = 'M %f, %f' %
+ (self.x*SCALE, -self.y*SCALE),
+ fill = color, stroke = 'none')
+ self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE))
+ self.move(x, y, resolve=False)
+
+ def fill_region(self):
+ self.dwg.add(self.region_path)
+ self.region_path = None
+
def dump(self, filename):
self.dwg.saveas(filename)
diff --git a/gerber/gerber.py b/gerber/rs274x.py
index 4ce261d..4076f77 100644
--- a/gerber/gerber.py
+++ b/gerber/rs274x.py
@@ -15,30 +15,35 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-"""
-Gerber File module
-==================
-**Gerber File module**
-
-This module provides an RS-274-X class and parser
+""" This module provides an RS-274-X class and parser.
"""
import re
import json
from .gerber_statements import *
-from .cnc import CncFile, FileSettings
+from .cam import CamFile, FileSettings
def read(filename):
""" Read data from filename and return a GerberFile
+
+ Parameters
+ ----------
+ filename : string
+ Filename of file to parse
+
+ Returns
+ -------
+ file : :class:`gerber.rs274x.GerberFile`
+ A GerberFile created from the specified file.
"""
return GerberParser().parse(filename)
-class GerberFile(CncFile):
+class GerberFile(CamFile):
""" A class representing a single gerber file
The GerberFile class represents a single gerber file.
@@ -86,24 +91,19 @@ class GerberFile(CncFile):
ybounds = [0.0, 0.0]
for stmt in [stmt for stmt in self.statements
if isinstance(stmt, CoordStmt)]:
- if stmt.x is not None and stmt.x < xbounds[0]:
- xbounds[0] = stmt.x
- if stmt.x is not None and stmt.x > xbounds[1]:
- xbounds[1] = stmt.x
- if stmt.i is not None and stmt.i < xbounds[0]:
- xbounds[0] = stmt.i
- if stmt.i is not None and stmt.i > xbounds[1]:
- xbounds[1] = stmt.i
- if stmt.y is not None and stmt.y < ybounds[0]:
- ybounds[0] = stmt.y
- if stmt.y is not None and stmt.y > ybounds[1]:
- ybounds[1] = stmt.y
- if stmt.j is not None and stmt.j < ybounds[0]:
- ybounds[0] = stmt.j
- if stmt.j is not None and stmt.j > ybounds[1]:
- ybounds[1] = stmt.j
+ if stmt.x is not None:
+ if stmt.x < xbounds[0]:
+ xbounds[0] = stmt.x
+ elif stmt.x > xbounds[1]:
+ xbounds[1] = stmt.x
+ if stmt.y is not None:
+ if stmt.y < ybounds[0]:
+ ybounds[0] = stmt.y
+ elif stmt.y > ybounds[1]:
+ ybounds[1] = stmt.y
return (xbounds, ybounds)
+
def write(self, filename):
""" Write data out to a gerber file
"""
@@ -113,6 +113,14 @@ class GerberFile(CncFile):
def render(self, ctx, filename=None):
""" Generate image of layer.
+
+ Parameters
+ ----------
+ ctx : :class:`GerberContext`
+ GerberContext subclass used for rendering the image
+
+ filename : string <optional>
+ If provided, the rendered image will be saved to `filename`
"""
ctx.set_bounds(self.bounds)
for statement in self.statements:
diff --git a/gerber/tests/test_cnc.py b/gerber/tests/test_cam.py
index ace047e..4af1984 100644
--- a/gerber/tests/test_cnc.py
+++ b/gerber/tests/test_cam.py
@@ -3,7 +3,7 @@
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
-from ..cnc import CncFile, FileSettings
+from ..cam import CamFile, FileSettings
from tests import *
@@ -46,5 +46,5 @@ def test_filesettings_assign():
assert_equal(fs.zero_suppression, 'test')
assert_equal(fs.format, 'test')
- def test_smoke_cncfile():
- pass
+def test_smoke_camfile():
+ cf = CamFile
diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py
index 9e73fd4..a463c9d 100644
--- a/gerber/tests/test_gerber_statements.py
+++ b/gerber/tests/test_gerber_statements.py
@@ -8,7 +8,7 @@ from ..gerber_statements import *
def test_FSParamStmt_factory():
- """ Test FSParamStruct factory correctly handles parameters
+ """ Test FSParamStruct factory
"""
stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'}
fs = FSParamStmt.from_dict(stmt)
@@ -24,6 +24,18 @@ def test_FSParamStmt_factory():
assert_equal(fs.notation, 'incremental')
assert_equal(fs.format, (2, 7))
+def test_FSParamStmt():
+ """ Test FSParamStmt initialization
+ """
+ param = 'FS'
+ zeros = 'trailing'
+ notation = 'absolute'
+ fmt = (2, 5)
+ stmt = FSParamStmt(param, zeros, notation, fmt)
+ assert_equal(stmt.param, param)
+ assert_equal(stmt.zero_suppression, zeros)
+ assert_equal(stmt.notation, notation)
+ assert_equal(stmt.format, fmt)
def test_FSParamStmt_dump():
""" Test FSParamStmt to_gerber()
@@ -38,17 +50,31 @@ def test_FSParamStmt_dump():
def test_MOParamStmt_factory():
- """ Test MOParamStruct factory correctly handles parameters
+ """ Test MOParamStruct factory
"""
- stmt = {'param': 'MO', 'mo': 'IN'}
- mo = MOParamStmt.from_dict(stmt)
- assert_equal(mo.param, 'MO')
- assert_equal(mo.mode, 'inch')
+ stmts = [{'param': 'MO', 'mo': 'IN'}, {'param': 'MO', 'mo': 'in'}, ]
+ for stmt in stmts:
+ mo = MOParamStmt.from_dict(stmt)
+ assert_equal(mo.param, 'MO')
+ assert_equal(mo.mode, 'inch')
+
+ stmts = [{'param': 'MO', 'mo': 'MM'}, {'param': 'MO', 'mo': 'mm'}, ]
+ for stmt in stmts:
+ mo = MOParamStmt.from_dict(stmt)
+ assert_equal(mo.param, 'MO')
+ assert_equal(mo.mode, 'metric')
+
+def test_MOParamStmt():
+ """ Test MOParamStmt initialization
+ """
+ param = 'MO'
+ mode = 'inch'
+ stmt = MOParamStmt(param, mode)
+ assert_equal(stmt.param, param)
- stmt = {'param': 'MO', 'mo': 'MM'}
- mo = MOParamStmt.from_dict(stmt)
- assert_equal(mo.param, 'MO')
- assert_equal(mo.mode, 'metric')
+ for mode in ['inch', 'metric']:
+ stmt = MOParamStmt(param, mode)
+ assert_equal(stmt.mode, mode)
def test_MOParamStmt_dump():
@@ -64,7 +90,7 @@ def test_MOParamStmt_dump():
def test_IPParamStmt_factory():
- """ Test IPParamStruct factory correctly handles parameters
+ """ Test IPParamStruct factory
"""
stmt = {'param': 'IP', 'ip': 'POS'}
ip = IPParamStmt.from_dict(stmt)
@@ -74,6 +100,15 @@ def test_IPParamStmt_factory():
ip = IPParamStmt.from_dict(stmt)
assert_equal(ip.ip, 'negative')
+def test_IPParamStmt():
+ """ Test IPParamStmt initialization
+ """
+ param = 'IP'
+ for ip in ['positive', 'negative']:
+ stmt = IPParamStmt(param, ip)
+ assert_equal(stmt.param, param)
+ assert_equal(stmt.ip, ip)
+
def test_IPParamStmt_dump():
""" Test IPParamStmt to_gerber()
@@ -88,14 +123,23 @@ def test_IPParamStmt_dump():
def test_OFParamStmt_factory():
- """ Test OFParamStmt factory correctly handles parameters
+ """ Test OFParamStmt factory
"""
stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'}
of = OFParamStmt.from_dict(stmt)
assert_equal(of.a, 0.1234567)
assert_equal(of.b, 0.1234567)
-
+def test_OFParamStmt():
+ """ Test IPParamStmt initialization
+ """
+ param = 'OF'
+ for val in [0.0, -3.4567]:
+ stmt = OFParamStmt(param, val, val)
+ assert_equal(stmt.param, param)
+ assert_equal(stmt.a, val)
+ assert_equal(stmt.b, val)
+
def test_OFParamStmt_dump():
""" Test OFParamStmt to_gerber()
"""
@@ -105,7 +149,7 @@ def test_OFParamStmt_dump():
def test_LPParamStmt_factory():
- """ Test LPParamStmt factory correctly handles parameters
+ """ Test LPParamStmt factory
"""
stmt = {'param': 'LP', 'lp': 'C'}
lp = LPParamStmt.from_dict(stmt)
@@ -128,7 +172,7 @@ def test_LPParamStmt_dump():
def test_INParamStmt_factory():
- """ Test INParamStmt factory correctly handles parameters
+ """ Test INParamStmt factory
"""
stmt = {'param': 'IN', 'name': 'test'}
inp = INParamStmt.from_dict(stmt)
@@ -143,7 +187,7 @@ def test_INParamStmt_dump():
def test_LNParamStmt_factory():
- """ Test LNParamStmt factory correctly handles parameters
+ """ Test LNParamStmt factory
"""
stmt = {'param': 'LN', 'name': 'test'}
lnp = LNParamStmt.from_dict(stmt)