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:
parent
b8dcc86cb4
commit
670d3fbbd7
5 changed files with 390 additions and 31 deletions
106
gerber/am_eval.py
Normal file
106
gerber/am_eval.py
Normal 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
229
gerber/am_read.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue