From 670d3fbbd7ebfb69bd223ac30b73ec47b195b380 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 3 Mar 2015 03:41:55 -0300 Subject: Add aperture macro parsing and evaluation. Aperture macros can get complex with arithmetical operations, variables and variables substitution. Current pcb-tools code just read each macro block as an independent unit, this cannot deal with variables that get changed after used. This patch splits the task in two: first we parse all macro content and creates a bytecode representation of all operations. This bytecode representation will be executed when an AD command is issues passing the required parameters. Parsing is heavily based on gerbv using a Shunting Yard approach to math parsing. Integration with rs274x.py code is not finished as I need to figure out how to integrate the final macro primitives with the graphical primitives already in use. --- gerber/am_eval.py | 106 ++++++++++++++++++++ gerber/am_read.py | 229 ++++++++++++++++++++++++++++++++++++++++++++ gerber/am_statements.py | 8 +- gerber/gerber_statements.py | 58 ++++++----- gerber/rs274x.py | 20 +++- 5 files changed, 390 insertions(+), 31 deletions(-) create mode 100644 gerber/am_eval.py create mode 100644 gerber/am_read.py 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 +# copyright 2014 Paulo Henrique Silva +# +# 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..d1201b2 --- /dev/null +++ b/gerber/am_read.py @@ -0,0 +1,229 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# copyright 2014 Paulo Henrique Silva +# +# 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 = "=" + + +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 "" + + 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 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("*"))) + else: + # decimal or integer disambiguation + if scanner.peek() not in '.': + 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: + 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:" + for opcode, argument in instructions: + print "%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else "") + + print "eval:" + for primitive in eval_macro(instructions): + print primitive diff --git a/gerber/am_statements.py b/gerber/am_statements.py index bdb12dd..c514ad7 100644 --- a/gerber/am_statements.py +++ b/gerber/am_statements.py @@ -406,9 +406,13 @@ class AMPolygonPrimitive(AMPrimitive): modifiers = primitive.strip(' *').split(",") code = int(modifiers[0]) exposure = "on" if modifiers[1].strip() == "1" else "off" - vertices = int(modifiers[2]) + 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) diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 89f4f84..19c7138 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,34 +343,37 @@ class AMParamStmt(ParamStmt): ParamStmt.__init__(self, param) self.name = name self.macro = macro - self.primitives = self._parsePrimitives(macro) - def _parsePrimitives(self, macro): + self.instructions = self.read(macro) + self.primitives = [] + + def read(self, macro): + return read_macro(macro) + + def evaluate(self, modifiers=[]): 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)) + for primitive in eval_macro(self.instructions, modifiers[0]): + 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 def to_inch(self): diff --git a/gerber/rs274x.py b/gerber/rs274x.py index c5c89fb..69485a8 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].evaluate(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): -- cgit