diff options
author | opiopan <opiopan@gmail.com> | 2019-03-21 22:00:32 +0900 |
---|---|---|
committer | opiopan <opiopan@gmail.com> | 2019-03-21 22:00:32 +0900 |
commit | 9febca7da6a730b3b3ca3a54129a9f88e5c44d14 (patch) | |
tree | 3f260096ab0c40eca527195630ab004208b4ee78 /gerberex | |
download | pcb-tools-extension-9febca7da6a730b3b3ca3a54129a9f88e5c44d14.tar.gz pcb-tools-extension-9febca7da6a730b3b3ca3a54129a9f88e5c44d14.tar.bz2 pcb-tools-extension-9febca7da6a730b3b3ca3a54129a9f88e5c44d14.zip |
initial commit
Diffstat (limited to 'gerberex')
-rw-r--r-- | gerberex/__init__.py | 8 | ||||
-rw-r--r-- | gerberex/am_expression.py | 183 | ||||
-rw-r--r-- | gerberex/am_primitive.py | 437 | ||||
-rw-r--r-- | gerberex/common.py | 35 | ||||
-rw-r--r-- | gerberex/composition.py | 201 | ||||
-rw-r--r-- | gerberex/dxf.py | 357 | ||||
-rw-r--r-- | gerberex/excellon.py | 42 | ||||
-rw-r--r-- | gerberex/rs274x.py | 25 | ||||
-rw-r--r-- | gerberex/statements.py | 34 |
9 files changed, 1322 insertions, 0 deletions
diff --git a/gerberex/__init__.py b/gerberex/__init__.py new file mode 100644 index 0000000..388f592 --- /dev/null +++ b/gerberex/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +from gerberex.common import (read, loads) +from gerberex.composition import (GerberComposition, DrillComposition) +from gerberex.dxf import DxfFile diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py new file mode 100644 index 0000000..7aa95c8 --- /dev/null +++ b/gerberex/am_expression.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +from gerber.utils import * +from gerber.am_eval import OpCode +from gerber.am_statements import * + +class AMExpression(object): + CONSTANT = 1 + VARIABLE = 2 + OPERATOR = 3 + + def __init__(self, kind): + self.kind = kind + + @property + def value(self): + return self + + def optimize(self): + pass + + def to_inch(self): + return AMOperatorExpression(AMOperatorExpression.DIV, self, + AMConstantExpression(MILLIMETERS_PER_INCH)) + + def to_metric(self): + return AMOperatorExpression(AMOperatorExpression.MUL, self, + AMConstantExpression(MILLIMETERS_PER_INCH)) + + def to_gerber(self, settings=None): + pass + + def to_instructions(self): + pass + +class AMConstantExpression(AMExpression): + def __init__(self, value): + super(AMConstantExpression, self).__init__(AMExpression.CONSTANT) + self._value = value + + @property + def value(self): + return self._value + + def optimize(self): + return self + + def to_gerber(self, settings=None): + return str(self._value) + + def to_instructions(self): + return [(OpCode.PUSH, self._value)] + +class AMVariableExpression(AMExpression): + def __init__(self, number): + super(AMVariableExpression, self).__init__(AMExpression.VARIABLE) + self.number = number + + def optimize(self): + return self + + def to_gerber(self, settings=None): + return '$%d' % self.number + + def to_instructions(self): + return (OpCode.LOAD, self.number) + +class AMOperatorExpression(AMExpression): + ADD = '+' + SUB = '-' + MUL = 'X' + DIV = '/' + + def __init__(self, op, lvalue, rvalue): + super(AMOperatorExpression, self).__init__(AMExpression.OPERATOR) + self.op = op + self.lvalue = lvalue + self.rvalue = rvalue + + def optimize(self): + self.lvalue = self.lvalue.optimize() + self.rvalue = self.rvalue.optimize() + + if isinstance(self.lvalue, AMConstantExpression) and isinstance(self.rvalue, AMConstantExpression): + lvalue = float(self.lvalue.value) + rvalue = float(self.rvalue.value) + value = lvalue + rvalue if self.op == self.ADD else \ + lvalue - rvalue if self.op == self.SUB else \ + lvalue * rvalue if self.op == self.MUL else \ + lvalue / rvalue if self.op == self.DIV else None + return AMConstantExpression(value) + elif self.op == self.ADD: + if self.rvalue.value == 0: + return self.lvalue + elif self.lvalue.value == 0: + return self.rvalue + elif self.op == self.SUB: + if self.rvalue.value == 0: + return self.lvalue + elif self.lvalue.value == 0 and isinstance(self.rvalue, AMConstantExpression): + return AMConstantExpression(-self.rvalue.value) + elif self.op == self.MUL: + if self.rvalue.value == 1: + return self.lvalue + elif self.lvalue.value == 1: + return self.rvalue + elif self.lvalue == 0 or self.rvalue == 0: + return AMConstantExpression(0) + elif self.op == self.DIV: + if self.rvalue.value == 1: + return self.lvalue + elif self.lvalue.value == 0: + return AMConstantExpression(0) + + return self + + def to_gerber(self, settings=None): + return '(%s)%s(%s)' % (self.lvalue.to_gerber(settings), self.op, self.rvalue.to_gerber(settings)) + + def to_instructions(self): + for i in self.lvalue.to_instructions(): + yield i + for i in self.rvalue.to_instructions(): + yield i + op = OpCode.ADD if self.op == self.ADD else\ + OpCode.SUB if self.op == self.SUB else\ + OpCode.MUL if self.op == self.MUL else\ + OpCode.DIV + yield (op, None) + +def eval_macro(instructions): + 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(AMConstantExpression(argument)) + + elif opcode == OpCode.LOAD: + push(AMVariableExpression(argument)) + + elif opcode == OpCode.STORE: + yield (-argument, [pop()]) + + elif opcode == OpCode.ADD: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.ADD, op2, op1)) + + elif opcode == OpCode.SUB: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.SUB, op2, op1)) + + elif opcode == OpCode.MUL: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.MUL, op2, op1)) + + elif opcode == OpCode.DIV: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.DIV, op2, op1)) + + elif opcode == OpCode.PRIM: + yield (argument, stack) + stack = [] + + diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py new file mode 100644 index 0000000..3ce047a --- /dev/null +++ b/gerberex/am_primitive.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +from gerber.utils import * +from gerber.am_statements import * +from gerber.am_eval import OpCode + +from gerberex.am_expression import eval_macro + +class AMPrimitiveDef(AMPrimitive): + def __init__(self, code, exposure=None, rotation=0): + super(AMPrimitiveDef, self).__init__(code, exposure) + self.rotation = rotation + + def to_inch(self): + pass + + def to_metric(self): + pass + + def to_gerber(self, settings=None): + pass + + def to_instructions(self): + pass + +class AMCommentPrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + return cls(code, modifiers[0]) + + def __init__(self, code, comment): + super(AMCommentPrimitiveDef, self).__init__(code) + self.comment = comment + + def to_gerber(self, settings=None): + return '%d %s*' % (self.code, self.comment.to_gerber()) + + def to_instructions(self): + return [(OpCode.PUSH, self.comment), (OpCode.PRIM, self.code)] + +class AMCirclePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + exposure = 'on' if modifiers[0] == 1 else 'off', + diameter = modifiers[1], + center_x = modifiers[2], + center_y = modifiers[3], + rotation = modifiers[4] + return cls(code, expressions, center_x, center_y, rotation) + + def __init__(self, code, exposure, diameter, center_x, center_y, rotation): + super(AMCirclePrimitiveDef, self).__init__(code, exposure, rotation) + self.diameter = diameter + self.center_x = center_x + self.center_y = center_y + + def to_inch(self): + self.diameter = self.diameter.to_inch().optimize() + self.center_x = self.center_x.to_inch().optimize() + self.center_y = self.center_y.to_inch().optimize() + + def to_metric(self): + self.diameter = self.diameter.to_metric().optimize() + self.center_x = self.center_x.to_metric().optimize() + self.center_y = self.center_y.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + diameter = self.diameter.to_gerber(settings), + x = self.center_x.to_gerber(settings), + y = self.center_y.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{diameter},{x},{y},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + for modifier in [self.diameter, self.center_x, self.center_y, self.rotation]: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMVectorLinePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' if modifiers[0] == 1 else 'off' + width = modifiers[1] + start_x = modifiers[2] + start_y = modifiers[3] + end_x = modifiers[4] + end_y = modifiers[5] + rotation = modifiers[6] + return cls(code, exposure, width, start_x, start_y, end_x, end_y, rotation) + + def __init__(self, code, exposure, width, start_x, start_y, end_x, end_y, rotation): + super(AMVectorLinePrimitiveDef, self).__init__(code, exposure, rotation) + self.width = width + self.start_x = start_x + self.start_y = start_y + self.end_x = end_x + self.end_y = end_y + + def to_inch(self): + self.width = self.width.to_inch().optimize() + self.start_x = self.start_x.to_inch().optimize() + self.start_y = self.start_y.to_inch().optimize() + self.end_x = self.end_x.to_inch().optimize() + self.end_y = self.end_x.to_inch().optimize() + + def to_metric(self): + self.width = self.width.to_metric().optimize() + self.start_x = self.start_x.to_metric().optimize() + self.start_y = self.start_y.to_metric().optimize() + self.end_x = self.end_x.to_metric().optimize() + self.end_y = self.end_x.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + width = self.width.to_gerber(settings), + start_x = self.start_x.to_gerber(settings), + start_y = self.start_y.to_gerber(settings), + end_x = self.end_x.to_gerber(settings), + end_y = self.end_y.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{width},{start_x},{start_y},{end_x},{end_y},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + modifiers = [self.width, self.start_x, self.start_y, self.end_x, self.end_y, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMCenterLinePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' if modifiers[0] == 1 else 'off' + width = modifiers[1] + height = modifiers[2] + x = modifiers[3] + y = modifiers[4] + rotation = modifiers[5] + return cls(code, exposure, width, height, x, y, rotation) + + def __init__(self, code, exposure, width, height, x, y, rotation): + super(AMCenterLinePrimitiveDef, self).__init__(code, exposure, rotation) + self.width = width + self.height = height + self.x = x + self.y = y + + def to_inch(self): + self.width = self.width.to_inch().optimize() + self.height = self.height.to_inch().optimize() + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + + def to_metric(self): + self.width = self.width.to_metric().optimize() + self.height = self.height.to_metric().optimize() + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + width = self.width.to_gerber(settings), + height = self.height.to_gerber(settings), + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{width},{height},{x},{y},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + modifiers = [self.width, self.height, self.x, self.y, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMOutlinePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + num_points = modifiers[1] + 1 + code = code + exposure = 'on' if modifiers[0] == 1 else 'off' + addrs = modifiers[2:num_points * 2] + rotation = modifiers[3 + num_points * 2] + return cls(code, exposure, addrs, rotation) + + def __init__(self, code, exposure, addrs, rotation): + super(AMOutlinePrimitiveDef, self).__init__(code, exposure, rotation) + self.addrs = addrs + + def to_inch(self): + self.addrs = [i.to_inch() for i in self.addrs] + + def to_metric(self): + self.addrs = [i.to_metric() for i in self.addrs] + + def to_gerber(self, settings=None): + def strs(): + yield '%d,%d,%d' % (self.code, + 1 if self.exposure == 'on' else 0, + len(self.addrs) / 2 - 1) + for i in self.addrs: + yield i.to_gerber(settings) + yield self.rotation.to_gerber(settings) + + return '%s*' % ','.join(strs()) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + yield (OpCode.PUSH, int(len(self.addrs) / 2 - 1)) + for modifier in self.addrs: + for i in modifier.to_instructions(): + yield i + for i in self.rotation.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMPolygonPrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' if modifiers[0] == 1 else 'off' + vertices = modifiers[1] + x = modifiers[2] + y = modifiers[3] + diameter = modifiers[4] + rotation = modifiers[5] + return cls(code, exposure, vertices, x, y, diameter, rotation) + + def __init__(self, code, exposure, vertices, x, y, diameter, rotation): + super(AMPolygonPrimitiveDef, self).__init__(code, exposure, rotation) + self.vertices = vertices + self.x = x + self.y = y + self.diameter = diameter + + def to_inch(self): + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + self.diameter = self.diameter.to_inch().optimize() + + def to_metric(self): + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + self.diameter = self.diameter.to_inch().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + vertices = self.vertices.to_gerber(settings), + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + diameter = self.diameter.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{vertices},{x},{y},{diameter},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + modifiers = [self.vertices, self.x, self.y, self.diameter, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMMoirePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' + x = modifiers[0] + y = modifiers[1] + diameter = modifiers[2] + ring_thickness = modifiers[3] + gap = modifiers[4] + max_rings = modifiers[5] + crosshair_thickness = modifiers[6] + crosshair_length = modifiers[7] + rotation = modifiers[8] + return cls(code, exposure, x, y, diameter, ring_thickness, gap, + max_rings, crosshair_thickness, crosshair_length, rotation) + + def __init__(self, code, exposure, x, y, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation): + super(AMMoirePrimitiveDef, self).__init__(code, exposure, rotation) + self.x = x + self.y = y + self.diameter = diameter + self.ring_thickness = ring_thickness + self.gap = gap + self.max_rings = max_rings + self.crosshair_thickness = crosshair_thickness + self.crosshair_length = crosshair_length + + def to_inch(self): + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + self.diameter = self.diameter.to_inch().optimize() + self.ring_thickness = self.ring_thickness.to_inch().optimize() + self.gap = self.gap.to_inch().optimize() + self.crosshair_thickness = self.crosshair_thickness.to_inch().optimize() + self.crosshair_length = self.crosshair_length.to_inch().optimize() + + def to_metric(self): + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + self.diameter = self.diameter.to_metric().optimize() + self.ring_thickness = self.ring_thickness.to_metric().optimize() + self.gap = self.gap.to_metric().optimize() + self.crosshair_thickness = self.crosshair_thickness.to_metric().optimize() + self.crosshair_length = self.crosshair_length.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + diameter = self.diameter.to_gerber(settings), + ring_thickness = self.ring_thickness.to_gerber(settings), + gap = self.gap.to_gerber(settings), + max_rings = self.max_rings.to_gerber(settings), + crosshair_thickness = self.crosshair_thickness.to_gerber(settings), + crosshair_length = self.crosshair_length.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{x},{y},{diameter},{ring_thickness},{gap},{max_rings},'\ + '{crosshair_thickness},{crosshair_length},{rotation}*'.format(**data) + + def to_instructions(self): + modifiers = [self.x, self.y, self.diameter, + self.ring_thickness, self.gap, self.max_rings, + self.crosshair_thickness, self.crosshair_length, + self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMThermalPrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' + x = modifiers[0] + y = modifiers[1] + outer_diameter = modifiers[2] + inner_diameter = modifiers[3] + gap = modifiers[4] + rotation = modifiers[5] + return cls(code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation) + + def __init__(self, code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation): + super(AMThermalPrimitiveDef, self).__init__(code, exposure, rotation) + self.x = x + self.y = y + self.outer_diameter = outer_diameter + self.inner_diameter = inner_diameter + self.gap = gap + + def to_inch(self): + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + self.outer_diameter = self.outer_diameter.to_inch().optimize() + self.inner_diameter = self.inner_diameter.to_inch().optimize() + self.gap = self.gap.to_inch().optimize() + + def to_metric(self): + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + self.outer_diameter = self.outer_diameter.to_metric().optimize() + self.inner_diameter = self.inner_diameter.to_metric().optimize() + self.gap = self.gap.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + outer_diameter = self.outer_diameter.to_gerber(settings), + inner_diameter = self.inner_diameter.to_gerber(settings), + gap = self.gap.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{x},{y},{outer_diameter},{inner_diameter},'\ + '{gap},{rotation}*'.format(**data) + + def to_instructions(self): + modifiers = [self.x, self.y, self.outer_diameter, + self.inner_diameter, self.gap, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMVariableDef(object): + def __init__(self, number, value): + self.number = number + self.value = value + + def to_inch(self): + return self + + def to_metric(self): + return self + + def to_gerber(self, settings=None): + return '$%d=%s*' % (self.number, self.value.to_gerber(settings)) + + def to_instructions(self): + for i in self.value.to_instructions(): + yield i + yield (OpCode.STORE, self.number) + +def to_primitive_defs(instructions): + classes = { + 0: AMCommentPrimitiveDef, + 1: AMCirclePrimitiveDef, + 2: AMVectorLinePrimitiveDef, + 20: AMVectorLinePrimitiveDef, + 21: AMCenterLinePrimitiveDef, + 4: AMOutlinePrimitiveDef, + 5: AMPolygonPrimitiveDef, + 6: AMMoirePrimitiveDef, + 7: AMThermalPrimitiveDef, + } + for code, modifiers in eval_macro(instructions): + if code < 0: + yield AMVariableDef(-code, modifiers[0]) + else: + primitive = classes[code] + yield primitive.from_modifiers(code, modifiers)
\ No newline at end of file diff --git a/gerberex/common.py b/gerberex/common.py new file mode 100644 index 0000000..4a85bd4 --- /dev/null +++ b/gerberex/common.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +import os +from gerber.common import loads as loads_org +from gerber.exceptions import ParseError +from gerber.utils import detect_file_format +import gerber.rs274x +import gerber.ipc356 +import gerberex.rs274x +import gerberex.excellon +import gerberex.dxf + +def read(filename, format=None): + with open(filename, 'rU') as f: + data = f.read() + return loads(data, filename, format=format) + + +def loads(data, filename=None, format=None): + if os.path.splitext(filename if filename else '')[1].lower() == '.dxf': + return gerberex.dxf.loads(data, filename) + + fmt = detect_file_format(data) + if fmt == 'rs274x': + file = gerber.rs274x.loads(data, filename=filename) + return gerberex.rs274x.GerberFile.from_gerber_file(file) + elif fmt == 'excellon': + return gerberex.excellon.loads(data, filename=filename, format=format) + elif fmt == 'ipc_d_356': + return ipc356.loads(data, filename=filename) + else: + raise ParseError('Unable to detect file format') diff --git a/gerberex/composition.py b/gerberex/composition.py new file mode 100644 index 0000000..2613a61 --- /dev/null +++ b/gerberex/composition.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> +import os +from functools import reduce +from gerber.cam import FileSettings +from gerber.gerber_statements import EofStmt +from gerber.excellon_statements import * +import gerberex.rs274x +import gerberex.excellon +import gerberex.dxf + +class Composition(object): + def __init__(self, settings = None, comments = None): + self.settings = settings + self.comments = comments if comments != None else [] + +class GerberComposition(Composition): + APERTURE_ID_BIAS = 10 + + def __init__(self, settings=None, comments=None): + super(GerberComposition, self).__init__(settings, comments) + self.param_statements = [] + self.aperture_macros = {} + self.apertures = [] + self.drawings = [] + + def merge(self, file): + if isinstance(file, gerberex.rs274x.GerberFile): + self._merge_gerber(file) + elif isinstance(file, gerberex.dxf.DxfFile): + self._merge_dxf(file) + else: + raise Exception('unsupported file type') + + def dump(self, path): + def statements(): + for s in self.param_statements: + yield s + for k in self.aperture_macros: + yield self.aperture_macros[k] + for s in self.apertures: + yield s + for s in self.drawings: + yield s + yield EofStmt() + with open(path, 'w') as f: + for statement in statements(): + f.write(statement.to_gerber(self.settings) + '\n') + + def _merge_gerber(self, file): + param_statements = [] + aperture_macro_map = {} + aperture_map = {} + + if self.settings: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + for statement in file.statements: + if statement.type == 'COMMENT': + self.comments.append(statement.comment) + elif statement.type == 'PARAM': + if statement.param == 'AM': + name = statement.name + newname = self._register_aperture_macro(statement) + aperture_macro_map[name] = newname + elif statement.param == 'AD': + if not statement.shape in ['C', 'R', 'O']: + statement.shape = aperture_macro_map[statement.shape] + dnum = statement.d + newdnum = self._register_aperture(statement) + aperture_map[dnum] = newdnum + elif statement.param == 'LP': + self.drawings.append(statement) + else: + param_statements.append(statement) + elif statement.type in ['EOF', "DEPRECATED"]: + pass + else: + if statement.type == 'APERTURE': + statement.d = aperture_map[statement.d] + self.drawings.append(statement) + + if not self.settings: + self.settings = file.settings + self.param_statements = param_statements + + def _merge_dxf(self, file): + if self.settings: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + file.dcode = self._register_aperture(file.aperture) + self.drawings.append(file.statements) + + if not self.settings: + self.settings = file.settings + self.param_statements = file.header + + + def _register_aperture_macro(self, statement): + name = statement.name + newname = name + offset = 0 + while newname in self.aperture_macros: + offset += 1 + newname = '%s_%d' % (name, offset) + statement.name = newname + self.aperture_macros[newname] = statement + return newname + + def _register_aperture(self, statement): + statement.d = len(self.apertures) + self.APERTURE_ID_BIAS + self.apertures.append(statement) + return statement.d + +class DrillComposition(Composition): + def __init__(self, settings=None, comments=None): + super(DrillComposition, self).__init__(settings, comments) + self.header1_statements = [] + self.header2_statements = [] + self.tools = [] + self.hits = [] + + def merge(self, file): + if isinstance(file, gerberex.excellon.ExcellonFileEx): + self._merge_excellon(file) + else: + raise Exception('unsupported file type') + + def dump(self, path): + def statements(): + for s in self.header1_statements: + yield s.to_excellon(self.settings) + for t in self.tools: + yield t.to_excellon(self.settings) + for s in self.header2_statements: + yield s.to_excellon(self.settings) + for t in self.tools: + yield ToolSelectionStmt(t.number).to_excellon(self.settings) + for h in self.hits: + if h.tool.number == t.number: + yield CoordinateStmt(*h.position).to_excellon(self.settings) + yield EndOfProgramStmt().to_excellon() + + with open(path, 'w') as f: + for statement in statements(): + f.write(statement + '\n') + + def _merge_excellon(self, file): + tool_map = {} + + if not self.settings: + self.settings = file.settings + else: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + if not self.header1_statements: + in_header1 = True + for statement in file.statements: + if not isinstance(statement, ToolSelectionStmt): + if isinstance(statement, ExcellonTool): + in_header1 = False + else: + if in_header1: + self.header1_statements.append(statement) + else: + self.header2_statements.append(statement) + else: + break + + for tool in iter(file.tools.values()): + num = tool.number + tool_map[num] = self._register_tool(tool) + + for hit in file.hits: + hit.tool = tool_map[hit.tool.number] + self.hits.append(hit) + + def _register_tool(self, tool): + for existing in self.tools: + if existing.equivalent(tool): + return existing + new_tool = ExcellonTool.from_tool(tool) + new_tool.settings = self.settings + def toolnums(): + for tool in self.tools: + yield tool.number + max_num = reduce(lambda x, y: x if x > y else y, toolnums(), 0) + new_tool.number = max_num + 1 + self.tools.append(new_tool) + return new_tool diff --git a/gerberex/dxf.py b/gerberex/dxf.py new file mode 100644 index 0000000..b641924 --- /dev/null +++ b/gerberex/dxf.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +import io +from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt +from gerber.cam import CamFile, FileSettings +from gerber.utils import inch, metric, write_gerber_value +from gerber.gerber_statements import ADParamStmt +import dxfgrabber + +class DxfStatement(object): + def __init__(self, entity): + self.entity = entity + + def to_gerber(self, settings=None): + pass + + def to_inch(self): + pass + + def to_metric(self): + pass + +class DxfLineStatement(DxfStatement): + def __init__(self, entity): + super(DxfLineStatement, self).__init__(entity) + + def to_gerber(self, settings=FileSettings): + x0 = self.entity.start[0] + y0 = self.entity.start[1] + x1 = self.entity.end[0] + y1 = self.entity.end[1] + return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format( + write_gerber_value(x0, settings.format, + settings.zero_suppression), + write_gerber_value(y0, settings.format, + settings.zero_suppression), + write_gerber_value(x1, settings.format, + settings.zero_suppression), + write_gerber_value(y1, settings.format, + settings.zero_suppression) + ) + + def to_inch(self): + self.entity.start[idx] = ( + inch(self.entity.start[idx][0]), inch(self.entity.start[idx][1])) + self.entity.end[idx] = ( + inch(self.entity.end[idx][0]), inch(self.entity.end[idx][1])) + + def to_metric(self): + self.entity.start[idx] = ( + metric(self.entity.start[idx][0]), inch(self.entity.start[idx][1])) + self.entity.end[idx] = ( + metric(self.entity.end[idx][0]), inch(self.entity.end[idx][1])) + +class DxfCircleStatement(DxfStatement): + def __init__(self, entity): + super(DxfCircleStatement, self).__init__(entity) + + def to_gerber(self, settings=FileSettings): + r = self.entity.radius + x0 = self.entity.center[0] + y0 = self.entity.center[1] + return 'G01*\nX{0}Y{1}D02*\n' \ + 'G75*\nG03*\nX{2}Y{3}I{4}J{5}D01*'.format( + write_gerber_value(x0 + r, settings.format, + settings.zero_suppression), + write_gerber_value(y0, settings.format, + settings.zero_suppression), + + write_gerber_value(x0 + r, settings.format, + settings.zero_suppression), + write_gerber_value(y0, settings.format, + settings.zero_suppression), + write_gerber_value(-r, settings.format, + settings.zero_suppression), + write_gerber_value(0, settings.format, + settings.zero_suppression) + ) + + def to_inch(self): + self.entity.radius = inch(self.entity.radius) + self.entity.center[idx] = ( + inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1])) + + def to_metric(self): + self.entity.radius = metric(self.entity.radius) + self.entity.center[idx] = ( + metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1])) + +class DxfArcStatement(DxfStatement): + def __init__(self, entity): + super(DxfArcStatement, self).__init__(entity) + + def to_gerber(self, settings=FileSettings): + deg0 = self.entity.start_angle + deg1 = self.entity.end_angle + r = self.entity.radius + x0 = self.entity.center[0] + y0 = self.entity.center[1] + begin_x = x0 + r * cos(deg0 / 180. * pi) + begin_y = y0 + r * sin(deg0 / 180. * pi) + end_x = x0 + r * cos(deg1 / 180. * pi) + end_y = y0 + r * sin(deg1 / 180. * pi) + + return 'G01*\nX{0}Y{1}D02*\n' \ + 'G75*\nG{2}*\nX{3}Y{4}I{5}J{6}D01*'.format( + write_gerber_value(begin_x, settings.format, + settings.zero_suppression), + write_gerber_value(begin_y, settings.format, + settings.zero_suppression), + '03' if deg0 > deg1 else '02', + write_gerber_value(end_x, settings.format, + settings.zero_suppression), + write_gerber_value(end_y, settings.format, + settings.zero_suppression), + write_gerber_value(x0 - begin_x, settings.format, + settings.zero_suppression), + write_gerber_value(y0 - begin_y, settings.format, + settings.zero_suppression) + ) + + def to_inch(self): + self.entity.start_angle = inch(self.entity.start_angle) + self.entity.end_angle = inch(self.entity.end_angle) + self.entity.radius = inch(self.entity.radius) + self.entity.center[idx] = ( + inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1])) + + def to_metric(self): + self.entity.start_angle = metric(self.entity.start_angle) + self.entity.end_angle = metric(self.entity.end_angle) + self.entity.radius = metric(self.entity.radius) + self.entity.center[idx] = ( + metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1])) + +class DxfPolylineStatement(DxfStatement): + def __init__(self, entity): + super(DxfPolylineStatement, self).__init__(entity) + + def to_gerber(self, settings=FileSettings()): + x0 = self.entity.points[0][0] + y0 = self.entity.points[0][1] + b = self.entity.bulge[0] + gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format( + write_gerber_value(x0, settings.format, + settings.zero_suppression), + write_gerber_value(y0, settings.format, + settings.zero_suppression), + ) + + def ptseq(): + for i in range(1, len(self.entity.points)): + yield i + if self.entity.is_closed: + yield 0 + + for idx in ptseq(): + pt = self.entity.points[idx] + x1 = pt[0] + y1 = pt[1] + if b == 0: + gerber += '\nG01*\nX{0}Y{1}D01*'.format( + write_gerber_value(x1, settings.format, + settings.zero_suppression), + write_gerber_value(y1, settings.format, + settings.zero_suppression), + ) + else: + ang = 4 * atan(b) + xm = x0 + x1 + ym = y0 + y1 + t = 1 / tan(ang / 2) + xc = (xm - t * (y1 - y0)) / 2 + yc = (ym + t * (x1 - x0)) / 2 + r = sqrt((x0 - xc)*(x0 - xc) + (y0 - yc)*(y0 - yc)) + + gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format( + '03' if ang > 0 else '02', + write_gerber_value(x1, settings.format, + settings.zero_suppression), + write_gerber_value(y1, settings.format, + settings.zero_suppression), + write_gerber_value(xc - x0, settings.format, + settings.zero_suppression), + write_gerber_value(yc - y0, settings.format, + settings.zero_suppression) + ) + + x0 = x1 + y0 = y1 + b = self.entity.bulge[idx] + + return gerber + + def to_inch(self): + for idx in range(0, len(self.entity.points)): + self.entity.points[idx] = ( + inch(self.entity.points[idx][0]), inch(self.entity.points[idx][1])) + self.entity.bulge[idx] = inch(self.entity.bulge[idx]) + + def to_metric(self): + for idx in range(0, len(self.entity.points)): + self.entity.points[idx] = ( + metric(self.entity.points[idx][0]), metric(self.entity.points[idx][1])) + self.entity.bulge[idx] = metric(self.entity.bulge[idx]) + +class DxfStatements(object): + def __init__(self, entities, units, dcode=10, draw_mode=None): + if draw_mode == None: + draw_mode = DxfFile.DM_LINE + self._units = units + self.dcode = dcode + self.draw_mode = draw_mode + self.statements = [] + for entity in entities: + if entity.dxftype == 'LWPOLYLINE': + self.statements.append(DxfPolylineStatement(entity)) + elif entity.dxftype == 'LINE': + self.statements.append(DxfLineStatement(entity)) + elif entity.dxftype == 'CIRCLE': + self.statements.append(DxfCircleStatement(entity)) + elif entity.dxftype == 'ARC': + self.statements.append(DxfArcStatement(entity)) + + @property + def units(self): + return _units + + def to_gerber(self, settings=FileSettings()): + def gerbers(): + yield 'D{0}*'.format(self.dcode) + if self.draw_mode == DxfFile.DM_FILL: + yield 'G36*' + for statement in self.statements: + if isinstance(statement, DxfCircleStatement) or \ + (isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed): + yield statement.to_gerber(settings) + yield 'G37*' + else: + for statement in self.statements: + yield statement.to_gerber(settings) + + return '\n'.join(gerbers()) + + def to_inch(self): + if self._units == 'metric': + self._units = 'inch' + for statement in self.statements: + statement.to_inch() + + def to_metric(self): + if self._units == 'inch': + self._units = 'metric' + for statement in self.statements: + statement.to_metric() + +class DxfHeaderStatement(object): + def to_gerber(self, settings): + return 'G75*\n'\ + '%MO{0}*%\n'\ + '%OFA0B0*%\n'\ + '%FS{1}AX{2}{3}Y{4}{5}*%\n'\ + '%IPPOS*%\n'\ + '%LPD*%'.format( + 'IN' if settings.units == 'inch' else 'MM', + 'L' if settings.zero_suppression == 'leading' else 'T', + settings.format[0], settings.format[1], + settings.format[0], settings.format[1] + ) + + def to_inch(self): + pass + + def to_metric(self): + pass + +class DxfFile(CamFile): + DM_LINE = 0 + DM_FILL = 1 + + def __init__(self, dxf, settings=FileSettings(), draw_mode=None, filename=None): + if draw_mode == None: + draw_mode = self.DM_LINE + if dxf.header['$INSUNITS'] == 1: + settings.units = 'inch' + settings.format = (2, 5) + else: + settings.units = 'metric' + settings.format = (3, 4) + + super(DxfFile, self).__init__(settings=settings, filename=filename) + self._draw_mode = draw_mode + self.header = DxfHeaderStatement() + self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0) + self.statements = DxfStatements(dxf.entities, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode) + + @property + def dcode(self): + return self.aperture.dcode + + @dcode.setter + def dcode(self, value): + self.aperture.d = value + self.statements.dcode = value + + @property + def width(self): + return self.aperture.modifiers[0][0] + + @width.setter + def width(self, value): + self.aperture.modifiers = ([float(value),],) + + @property + def draw_mode(self): + return self._draw_mode + + @draw_mode.setter + def draw_mode(self, value): + self._draw_mode = value + self.statements.draw_mode = value + + def write(self, filename=None): + if self.settings.notation != 'absolute': + raise Exception('DXF file\'s notation must be absolute ') + + filename = filename if filename is not None else self.filename + with open(filename, 'w') as f: + f.write(self.header.to_gerber(self.settings) + '\n') + f.write(self.aperture.to_gerber(self.settings) + '\n') + f.write(self.statements.to_gerber(self.settings) + '\n') + f.write('M02*\n') + + def to_inch(self): + if self.units == 'metric': + self.header.to_inch() + self.aperture.to_inch() + self.statements.to_inch() + self.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + self.header.to_metric() + self.aperture.to_metric() + self.statements.to_metric() + self.units = 'metric' + + def offset(self, ofset_x, offset_y): + raise Exception('Not supported') + +def loads(data, filename=None): + stream = io.StringIO(data) + dxf = dxfgrabber.read(stream) + return DxfFile(dxf) diff --git a/gerberex/excellon.py b/gerberex/excellon.py new file mode 100644 index 0000000..78e6e5f --- /dev/null +++ b/gerberex/excellon.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) +from gerber.excellon_statements import UnitStmt +from gerber.cam import FileSettings + +def loads(data, filename=None, settings=None, tools=None, format=None): + if not settings: + settings = FileSettings(**detect_excellon_format(data)) + if format: + settings.format = format + file = ExcellonParser(settings, tools).parse_raw(data, filename) + return ExcellonFileEx.from_file(file) + +class ExcellonFileEx(ExcellonFile): + @classmethod + def from_file(cls, file): + statements = [ + UnitStmtEx.from_statement(s) if isinstance(s, UnitStmt) else s \ + for s in file.statements + ] + return cls(statements, file.tools, file.hits, file.settings, file.filename) + + def __init__(self, statements, tools, hits, settings, filename=None): + super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename) + +class UnitStmtEx(UnitStmt): + @classmethod + def from_statement(cls, stmt): + return cls(units=stmt.units, zeros=stmt.zeros, format=stmt.format, id=stmt.id) + + def __init__(self, units='inch', zeros='leading', format=None, **kwargs): + super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs) + + def to_excellon(self, settings=None): + stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC', + 'LZ' if self.zeros == 'leading' else 'TZ', + '0' * self.format[0], '0' * self.format[1]) + return stmt diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py new file mode 100644 index 0000000..e9d82cd --- /dev/null +++ b/gerberex/rs274x.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +import gerber.rs274x +from gerberex.statements import (AMParamStmt, AMParamStmtEx) + +class GerberFile(gerber.rs274x.GerberFile): + @classmethod + def from_gerber_file(cls, gerber_file): + if not isinstance(gerber_file, gerber.rs274x.GerberFile): + raise Exception('only gerber.rs274x.GerberFile object is specified') + + def swap_statement(statement): + if isinstance(statement, AMParamStmt) and not isinstance(statement, AMParamStmtEx): + return AMParamStmtEx.from_stmt(statement) + else: + return statement + statements = [swap_statement(statement) for statement in gerber_file.statements] + return cls(statements, gerber_file.settings, gerber_file.primitives,\ + gerber_file.apertures, gerber_file.filename) + + def __init__(self, statements, settings, primitives, apertures, filename=None): + super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename) diff --git a/gerberex/statements.py b/gerberex/statements.py new file mode 100644 index 0000000..77ca235 --- /dev/null +++ b/gerberex/statements.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> + +from gerber.gerber_statements import AMParamStmt +from gerberex.am_primitive import to_primitive_defs + +class AMParamStmtEx(AMParamStmt): + @classmethod + def from_stmt(cls, stmt): + return cls(stmt.param, stmt.name, stmt.macro) + + def __init__(self, param, name, macro): + super(AMParamStmtEx, self).__init__(param, name, macro) + self.primitive_defs = to_primitive_defs(self.instructions) + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + for p in self.primitive_defs: + p.to_inch() + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + for p in self.primitive_defs: + p.to_metric() + + def to_gerber(self, settings = None): + def plist(): + for p in self.primitive_defs: + yield p.to_gerber(settings) + return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist())) |