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.
This commit is contained in:
Paulo Henrique Silva 2015-03-03 03:41:55 -03:00
parent b8dcc86cb4
commit 670d3fbbd7
5 changed files with 390 additions and 31 deletions

106
gerber/am_eval.py Normal file
View file

@ -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 = []

229
gerber/am_read.py Normal file
View file

@ -0,0 +1,229 @@
#! /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 = "="
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

View file

@ -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)

View file

@ -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):

View file

@ -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):