diff options
author | Hamilton Kibbe <hamilton.kibbe@gmail.com> | 2015-03-05 13:33:49 -0500 |
---|---|---|
committer | Hamilton Kibbe <hamilton.kibbe@gmail.com> | 2015-03-05 13:33:49 -0500 |
commit | c40683b6a216f29fe473c31680ade7ab294002cd (patch) | |
tree | 2e02290c8b5e295c3e3cb933e1b60df5bc43ddbb | |
parent | c542493b9b84a6af204c011bb9fc02eb43e48b2b (diff) | |
parent | 21fdb9cb57f5da938084fbf2b8133d903d0b0d77 (diff) | |
download | gerbonara-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.py | 106 | ||||
-rw-r--r-- | gerber/am_read.py | 236 | ||||
-rw-r--r-- | gerber/am_statements.py | 24 | ||||
-rw-r--r-- | gerber/gerber_statements.py | 63 | ||||
-rw-r--r-- | gerber/rs274x.py | 20 | ||||
-rw-r--r-- | gerber/tests/test_gerber_statements.py | 16 |
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'} |