summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHamilton Kibbe <hamilton.kibbe@gmail.com>2015-03-05 13:33:49 -0500
committerHamilton Kibbe <hamilton.kibbe@gmail.com>2015-03-05 13:33:49 -0500
commitc40683b6a216f29fe473c31680ade7ab294002cd (patch)
tree2e02290c8b5e295c3e3cb933e1b60df5bc43ddbb
parentc542493b9b84a6af204c011bb9fc02eb43e48b2b (diff)
parent21fdb9cb57f5da938084fbf2b8133d903d0b0d77 (diff)
downloadgerbonara-c40683b6a216f29fe473c31680ade7ab294002cd.tar.gz
gerbonara-c40683b6a216f29fe473c31680ade7ab294002cd.tar.bz2
gerbonara-c40683b6a216f29fe473c31680ade7ab294002cd.zip
Merge pull request #23 from curtacircuitos/macro-parse-eval
Add aperture macro parsing and evaluation.
-rw-r--r--gerber/am_eval.py106
-rw-r--r--gerber/am_read.py236
-rw-r--r--gerber/am_statements.py24
-rw-r--r--gerber/gerber_statements.py63
-rw-r--r--gerber/rs274x.py20
-rw-r--r--gerber/tests/test_gerber_statements.py16
6 files changed, 418 insertions, 47 deletions
diff --git a/gerber/am_eval.py b/gerber/am_eval.py
new file mode 100644
index 0000000..29b380d
--- /dev/null
+++ b/gerber/am_eval.py
@@ -0,0 +1,106 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# copyright 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
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# 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.
+""" This module provides RS-274-X AM macro evaluation.
+"""
+
+class OpCode:
+ PUSH = 1
+ LOAD = 2
+ STORE = 3
+ ADD = 4
+ SUB = 5
+ MUL = 6
+ DIV = 7
+ PRIM = 8
+
+ @staticmethod
+ def str(opcode):
+ if opcode == OpCode.PUSH:
+ return "OPCODE_PUSH"
+ elif opcode == OpCode.LOAD:
+ return "OPCODE_LOAD"
+ elif opcode == OpCode.STORE:
+ return "OPCODE_STORE"
+ elif opcode == OpCode.ADD:
+ return "OPCODE_ADD"
+ elif opcode == OpCode.SUB:
+ return "OPCODE_SUB"
+ elif opcode == OpCode.MUL:
+ return "OPCODE_MUL"
+ elif opcode == OpCode.DIV:
+ return "OPCODE_DIV"
+ elif opcode == OpCode.PRIM:
+ return "OPCODE_PRIM"
+ else:
+ return "UNKNOWN"
+
+def eval_macro(instructions, parameters={}):
+
+ if not isinstance(parameters, type({})):
+ p = {}
+ for i, val in enumerate(parameters):
+ p[i+1] = val
+
+ parameters = p
+
+ stack = []
+ def pop():
+ return stack.pop()
+
+ def push(op):
+ stack.append(op)
+
+ def top():
+ return stack[-1]
+
+ def empty():
+ return len(stack) == 0
+
+ for opcode, argument in instructions:
+ if opcode == OpCode.PUSH:
+ push(argument)
+
+ elif opcode == OpCode.LOAD:
+ push(parameters.get(argument, 0))
+
+ elif opcode == OpCode.STORE:
+ parameters[argument] = pop()
+
+ elif opcode == OpCode.ADD:
+ op1 = pop()
+ op2 = pop()
+ push(op2 + op1)
+
+ elif opcode == OpCode.SUB:
+ op1 = pop()
+ op2 = pop()
+ push(op2 - op2)
+
+ elif opcode == OpCode.MUL:
+ op1 = pop()
+ op2 = pop()
+ push(op2 * op1)
+
+ elif opcode == OpCode.DIV:
+ op1 = pop()
+ op2 = pop()
+ push(op2 / op1)
+
+ elif opcode == OpCode.PRIM:
+ yield "%d,%s" % (argument, ",".join([str(x) for x in stack]))
+ stack = []
diff --git a/gerber/am_read.py b/gerber/am_read.py
new file mode 100644
index 0000000..05f3343
--- /dev/null
+++ b/gerber/am_read.py
@@ -0,0 +1,236 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# copyright 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
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# 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.
+""" This module provides RS-274-X AM macro modifiers parsing.
+"""
+
+from .am_eval import OpCode, eval_macro
+
+import string
+
+
+class Token:
+ ADD = "+"
+ SUB = "-"
+ MULT = ("x", "X") # compatibility as many gerber writes do use non compliant X
+ DIV = "/"
+ OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV)
+ LEFT_PARENS = "("
+ RIGHT_PARENS = ")"
+ EQUALS = "="
+ EOF = "EOF"
+
+
+def token_to_opcode(token):
+ if token == Token.ADD:
+ return OpCode.ADD
+ elif token == Token.SUB:
+ return OpCode.SUB
+ elif token in Token.MULT:
+ return OpCode.MUL
+ elif token == Token.DIV:
+ return OpCode.DIV
+ else:
+ return None
+
+
+def precedence(token):
+ if token == Token.ADD or token == Token.SUB:
+ return 1
+ elif token in Token.MULT or token == Token.DIV:
+ return 2
+ else:
+ return 0
+
+
+def is_op(token):
+ return token in Token.OPERATORS
+
+
+class Scanner:
+ def __init__(self, s):
+ self.buff = s
+ self.n = 0
+
+ def eof(self):
+ return self.n == len(self.buff)
+
+ def peek(self):
+ if not self.eof():
+ return self.buff[self.n]
+
+ return Token.EOF
+
+ def ungetc(self):
+ if self.n > 0:
+ self.n -= 1
+
+ def getc(self):
+ if self.eof():
+ return ""
+
+ c = self.buff[self.n]
+ self.n += 1
+ return c
+
+ def readint(self):
+ n = ""
+ while not self.eof() and (self.peek() in string.digits):
+ n += self.getc()
+ return int(n)
+
+ def readfloat(self):
+ n = ""
+ while not self.eof() and (self.peek() in string.digits or self.peek() == "."):
+ n += self.getc()
+ return float(n)
+
+ def readstr(self, end="*"):
+ s = ""
+ while not self.eof() and self.peek() != end:
+ s += self.getc()
+ return s.strip()
+
+
+def print_instructions(instructions):
+ for opcode, argument in instructions:
+ print("%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else ""))
+
+
+def read_macro(macro):
+
+ instructions = []
+
+ for block in macro.split("*"):
+
+ is_primitive = False
+ is_equation = False
+
+ found_equation_left_side = False
+ found_primitive_code = False
+
+ equation_left_side = 0
+ primitive_code = 0
+
+ if Token.EQUALS in block:
+ is_equation = True
+ else:
+ is_primitive = True
+
+ scanner = Scanner(block)
+
+ # inlined here for compactness and convenience
+ op_stack = []
+
+ def pop():
+ return op_stack.pop()
+
+ def push(op):
+ op_stack.append(op)
+
+ def top():
+ return op_stack[-1]
+
+ def empty():
+ return len(op_stack) == 0
+
+ while not scanner.eof():
+
+ c = scanner.getc()
+
+ if c == ",":
+ found_primitive_code = True
+
+ # add all instructions on the stack to finish last modifier
+ while not empty():
+ instructions.append((token_to_opcode(pop()), None))
+
+ elif c in Token.OPERATORS:
+ while not empty() and is_op(top()) and precedence(top()) >= precedence(c):
+ instructions.append((token_to_opcode(pop()), None))
+
+ push(c)
+
+ elif c == Token.LEFT_PARENS:
+ push(c)
+
+ elif c == Token.RIGHT_PARENS:
+ while not empty() and top() != Token.LEFT_PARENS:
+ instructions.append((token_to_opcode(pop()), None))
+
+ if empty():
+ raise ValueError("unbalanced parentheses")
+
+ # discard "("
+ pop()
+
+ elif c.startswith("$"):
+ n = scanner.readint()
+
+ if is_equation and not found_equation_left_side:
+ equation_left_side = n
+ else:
+ instructions.append((OpCode.LOAD, n))
+
+ elif c == Token.EQUALS:
+ found_equation_left_side = True
+
+ elif c == "0":
+ if is_primitive and not found_primitive_code:
+ instructions.append((OpCode.PUSH, scanner.readstr("*")))
+ found_primitive_code = True
+ else:
+ # decimal or integer disambiguation
+ if scanner.peek() not in '.' or scanner.peek() == Token.EOF:
+ instructions.append((OpCode.PUSH, 0))
+
+ elif c in "123456789.":
+ scanner.ungetc()
+
+ if is_primitive and not found_primitive_code:
+ primitive_code = scanner.readint()
+ else:
+ instructions.append((OpCode.PUSH, scanner.readfloat()))
+
+ else:
+ # whitespace or unknown char
+ pass
+
+ # add all instructions on the stack to finish last modifier (if any)
+ while not empty():
+ instructions.append((token_to_opcode(pop()), None))
+
+ # at end, we either have a primitive or a equation
+ if is_primitive and found_primitive_code:
+ instructions.append((OpCode.PRIM, primitive_code))
+
+ if is_equation:
+ instructions.append((OpCode.STORE, equation_left_side))
+
+ return instructions
+
+if __name__ == '__main__':
+ import sys
+
+ instructions = read_macro(sys.argv[1])
+
+ print("insructions:")
+ print_instructions(instructions)
+
+ print("eval:")
+ for primitive in eval_macro(instructions):
+ print(primitive)
diff --git a/gerber/am_statements.py b/gerber/am_statements.py
index bdb12dd..38f4d71 100644
--- a/gerber/am_statements.py
+++ b/gerber/am_statements.py
@@ -160,7 +160,7 @@ class AMCirclePrimitive(AMPrimitive):
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(',')
code = int(modifiers[0])
- exposure = 'on' if modifiers[1].strip() == '1' else 'off'
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
diameter = float(modifiers[2])
position = (float(modifiers[3]), float(modifiers[4]))
return cls(code, exposure, diameter, position)
@@ -233,7 +233,7 @@ class AMVectorLinePrimitive(AMPrimitive):
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(',')
code = int(modifiers[0])
- exposure = 'on' if modifiers[1].strip() == '1' else 'off'
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
width = float(modifiers[2])
start = (float(modifiers[3]), float(modifiers[4]))
end = (float(modifiers[5]), float(modifiers[6]))
@@ -318,8 +318,8 @@ class AMOutlinePrimitive(AMPrimitive):
modifiers = primitive.strip(' *').split(",")
code = int(modifiers[0])
- exposure = "on" if modifiers[1].strip() == "1" else "off"
- n = int(modifiers[2])
+ exposure = "on" if float(modifiers[1]) == 1 else "off"
+ n = int(float(modifiers[2]))
start_point = (float(modifiers[3]), float(modifiers[4]))
points = []
for i in range(n):
@@ -405,10 +405,14 @@ class AMPolygonPrimitive(AMPrimitive):
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(",")
code = int(modifiers[0])
- exposure = "on" if modifiers[1].strip() == "1" else "off"
- vertices = int(modifiers[2])
+ exposure = "on" if float(modifiers[1]) == 1 else "off"
+ vertices = int(float(modifiers[2]))
position = (float(modifiers[3]), float(modifiers[4]))
- diameter = float(modifiers[5])
+ try:
+ diameter = float(modifiers[5])
+ except:
+ diameter = 0
+
rotation = float(modifiers[6])
return cls(code, exposure, vertices, position, diameter, rotation)
@@ -504,7 +508,7 @@ class AMMoirePrimitive(AMPrimitive):
diameter = float(modifiers[3])
ring_thickness = float(modifiers[4])
gap = float(modifiers[5])
- max_rings = int(modifiers[6])
+ max_rings = int(float(modifiers[6]))
crosshair_thickness = float(modifiers[7])
crosshair_length = float(modifiers[8])
rotation = float(modifiers[9])
@@ -686,7 +690,7 @@ class AMCenterLinePrimitive(AMPrimitive):
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(",")
code = int(modifiers[0])
- exposure = 'on' if modifiers[1].strip() == '1' else 'off'
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
width = float(modifiers[2])
height = float(modifiers[3])
center= (float(modifiers[4]), float(modifiers[5]))
@@ -768,7 +772,7 @@ class AMLowerLeftLinePrimitive(AMPrimitive):
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(",")
code = int(modifiers[0])
- exposure = 'on' if modifiers[1].strip() == '1' else 'off'
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
width = float(modifiers[2])
height = float(modifiers[3])
lower_left = (float(modifiers[4]), float(modifiers[5]))
diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py
index 89f4f84..99672de 100644
--- a/gerber/gerber_statements.py
+++ b/gerber/gerber_statements.py
@@ -22,7 +22,10 @@ Gerber (RS-274X) Statements
"""
from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
inch, metric)
+
from .am_statements import *
+from .am_read import read_macro
+from .am_eval import eval_macro
class Statement(object):
@@ -340,35 +343,37 @@ class AMParamStmt(ParamStmt):
ParamStmt.__init__(self, param)
self.name = name
self.macro = macro
- self.primitives = self._parsePrimitives(macro)
-
- def _parsePrimitives(self, macro):
- primitives = []
- for primitive in macro.strip('%\n').split('*'):
- # Couldn't find anything explicit about leading whitespace in the spec...
- primitive = primitive.strip(' *%\n')
- if len(primitive):
- if primitive[0] == '0':
- primitives.append(AMCommentPrimitive.from_gerber(primitive))
- elif primitive[0] == '1':
- primitives.append(AMCirclePrimitive.from_gerber(primitive))
- elif primitive[0:2] in ('2,', '20'):
- primitives.append(AMVectorLinePrimitive.from_gerber(primitive))
- elif primitive[0:2] == '21':
- primitives.append(AMCenterLinePrimitive.from_gerber(primitive))
- elif primitive[0:2] == '22':
- primitives.append(AMLowerLeftLinePrimitive.from_gerber(primitive))
- elif primitive[0] == '4':
- primitives.append(AMOutlinePrimitive.from_gerber(primitive))
- elif primitive[0] == '5':
- primitives.append(AMPolygonPrimitive.from_gerber(primitive))
- elif primitive[0] =='6':
- primitives.append(AMMoirePrimitive.from_gerber(primitive))
- elif primitive[0] == '7':
- primitives.append(AMThermalPrimitive.from_gerber(primitive))
- else:
- primitives.append(AMUnsupportPrimitive.from_gerber(primitive))
- return primitives
+
+ self.instructions = self.read(macro)
+ self.primitives = []
+
+ def read(self, macro):
+ return read_macro(macro)
+
+ def build(self, modifiers=[[]]):
+ self.primitives = []
+
+ for primitive in eval_macro(self.instructions, modifiers[0]):
+ if primitive[0] == '0':
+ self.primitives.append(AMCommentPrimitive.from_gerber(primitive))
+ elif primitive[0] == '1':
+ self.primitives.append(AMCirclePrimitive.from_gerber(primitive))
+ elif primitive[0:2] in ('2,', '20'):
+ self.primitives.append(AMVectorLinePrimitive.from_gerber(primitive))
+ elif primitive[0:2] == '21':
+ self.primitives.append(AMCenterLinePrimitive.from_gerber(primitive))
+ elif primitive[0:2] == '22':
+ self.primitives.append(AMLowerLeftLinePrimitive.from_gerber(primitive))
+ elif primitive[0] == '4':
+ self.primitives.append(AMOutlinePrimitive.from_gerber(primitive))
+ elif primitive[0] == '5':
+ self.primitives.append(AMPolygonPrimitive.from_gerber(primitive))
+ elif primitive[0] =='6':
+ self.primitives.append(AMMoirePrimitive.from_gerber(primitive))
+ elif primitive[0] == '7':
+ self.primitives.append(AMThermalPrimitive.from_gerber(primitive))
+ else:
+ self.primitives.append(AMUnsupportPrimitive.from_gerber(primitive))
def to_inch(self):
for primitive in self.primitives:
diff --git a/gerber/rs274x.py b/gerber/rs274x.py
index c5c89fb..a3a27e9 100644
--- a/gerber/rs274x.py
+++ b/gerber/rs274x.py
@@ -189,6 +189,7 @@ class GerberParser(object):
self.statements = []
self.primitives = []
self.apertures = {}
+ self.macros = {}
self.current_region = None
self.x = 0
self.y = 0
@@ -392,6 +393,12 @@ class GerberParser(object):
width = modifiers[0][0]
height = modifiers[0][1]
aperture = Obround(position=None, width=width, height=height)
+ elif shape == 'P':
+ # FIXME: not supported yet?
+ pass
+ else:
+ aperture = self.macros[shape].build(modifiers)
+
self.apertures[d] = aperture
def _evaluate_mode(self, stmt):
@@ -414,6 +421,8 @@ class GerberParser(object):
self.image_polarity = stmt.ip
elif stmt.param == "LP":
self.level_polarity = stmt.lp
+ elif stmt.param == "AM":
+ self.macros[stmt.name] = stmt
elif stmt.param == "AD":
self._define_aperture(stmt.d, stmt.shape, stmt.modifiers)
@@ -449,9 +458,14 @@ class GerberParser(object):
primitive = copy.deepcopy(self.apertures[self.aperture])
# XXX: temporary fix because there are no primitives for Macros and Polygon
if primitive is not None:
- primitive.position = (x, y)
- primitive.level_polarity = self.level_polarity
- self.primitives.append(primitive)
+ # XXX: just to make it easy to spot
+ if isinstance(primitive, type([])):
+ print(primitive[0].to_gerber())
+ else:
+ primitive.position = (x, y)
+ primitive.level_polarity = self.level_polarity
+ self.primitives.append(primitive)
+
self.x, self.y = x, y
def _evaluate_aperture(self, stmt):
diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py
index 04358eb..9032268 100644
--- a/gerber/tests/test_gerber_statements.py
+++ b/gerber/tests/test_gerber_statements.py
@@ -333,6 +333,7 @@ def test_AMParamStmt_factory():
8,THIS IS AN UNSUPPORTED PRIMITIVE*
''')
s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s.build()
assert_equal(len(s.primitives), 10)
assert_true(isinstance(s.primitives[0], AMCommentPrimitive))
assert_true(isinstance(s.primitives[1], AMCirclePrimitive))
@@ -347,29 +348,34 @@ def test_AMParamStmt_factory():
def testAMParamStmt_conversion():
name = 'POLYGON'
- macro = '5,1,8,25.4,25.4,25.4,0*%'
+ macro = '5,1,8,25.4,25.4,25.4,0*'
s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s.build()
s.to_inch()
assert_equal(s.primitives[0].position, (1., 1.))
assert_equal(s.primitives[0].diameter, 1.)
- macro = '5,1,8,1,1,1,0*%'
+ macro = '5,1,8,1,1,1,0*'
s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s.build()
s.to_metric()
assert_equal(s.primitives[0].position, (25.4, 25.4))
assert_equal(s.primitives[0].diameter, 25.4)
def test_AMParamStmt_dump():
name = 'POLYGON'
- macro = '5,1,8,25.4,25.4,25.4,0*%'
+ macro = '5,1,8,25.4,25.4,25.4,0*'
s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s.build()
+
assert_equal(s.to_gerber(), '%AMPOLYGON*5,1,8,25.4,25.4,25.4,0.0*%')
def test_AMParamStmt_string():
name = 'POLYGON'
- macro = '5,1,8,25.4,25.4,25.4,0*%'
+ macro = '5,1,8,25.4,25.4,25.4,0*'
s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
- assert_equal(str(s), '<Aperture Macro POLYGON: 5,1,8,25.4,25.4,25.4,0*%>')
+ s.build()
+ assert_equal(str(s), '<Aperture Macro POLYGON: 5,1,8,25.4,25.4,25.4,0*>')
def test_ASParamStmt_factory():
stmt = {'param': 'AS', 'mode': 'AXBY'}