From 9febca7da6a730b3b3ca3a54129a9f88e5c44d14 Mon Sep 17 00:00:00 2001 From: opiopan Date: Thu, 21 Mar 2019 22:00:32 +0900 Subject: initial commit --- gerberex/__init__.py | 8 + gerberex/am_expression.py | 183 +++++++++++++++++++ gerberex/am_primitive.py | 437 ++++++++++++++++++++++++++++++++++++++++++++++ gerberex/common.py | 35 ++++ gerberex/composition.py | 201 +++++++++++++++++++++ gerberex/dxf.py | 357 +++++++++++++++++++++++++++++++++++++ gerberex/excellon.py | 42 +++++ gerberex/rs274x.py | 25 +++ gerberex/statements.py | 34 ++++ 9 files changed, 1322 insertions(+) create mode 100644 gerberex/__init__.py create mode 100644 gerberex/am_expression.py create mode 100644 gerberex/am_primitive.py create mode 100644 gerberex/common.py create mode 100644 gerberex/composition.py create mode 100644 gerberex/dxf.py create mode 100644 gerberex/excellon.py create mode 100644 gerberex/rs274x.py create mode 100644 gerberex/statements.py (limited to 'gerberex') 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 + +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 + +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 + +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 + +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 +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 + +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 + +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 + +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 + +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())) -- cgit From 690df56bb71020901167605a87ec451081fa18d7 Mon Sep 17 00:00:00 2001 From: opiopan Date: Sat, 23 Mar 2019 21:59:13 +0900 Subject: add rotation fuction --- gerberex/am_primitive.py | 35 +++++++++++++++++--------- gerberex/composition.py | 2 +- gerberex/dxf.py | 2 +- gerberex/excellon.py | 7 ++++++ gerberex/rs274x.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++- gerberex/statements.py | 38 +++++++++++++++++++++++++++- gerberex/utility.py | 13 ++++++++++ 7 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 gerberex/utility.py (limited to 'gerberex') diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py index 3ce047a..82370f6 100644 --- a/gerberex/am_primitive.py +++ b/gerberex/am_primitive.py @@ -7,13 +7,21 @@ from gerber.utils import * from gerber.am_statements import * from gerber.am_eval import OpCode -from gerberex.am_expression import eval_macro +from gerberex.am_expression import eval_macro, AMConstantExpression, AMOperatorExpression class AMPrimitiveDef(AMPrimitive): - def __init__(self, code, exposure=None, rotation=0): + def __init__(self, code, exposure=None, rotation=None): super(AMPrimitiveDef, self).__init__(code, exposure) + if not rotation: + rotation = AMConstantExpression(0) self.rotation = rotation + def rotate(self, angle, center=None): + self.rotation = AMOperatorExpression(AMOperatorExpression.ADD, + self.rotation, + AMConstantExpression(float(angle))) + self.rotation = self.rotation.optimize() + def to_inch(self): pass @@ -44,12 +52,12 @@ class AMCommentPrimitiveDef(AMPrimitiveDef): 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], + exposure = 'on' if modifiers[0].value == 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) + return cls(code, exposure, diameter, center_x, center_y, rotation) def __init__(self, code, exposure, diameter, center_x, center_y, rotation): super(AMCirclePrimitiveDef, self).__init__(code, exposure, rotation) @@ -87,7 +95,7 @@ class AMVectorLinePrimitiveDef(AMPrimitiveDef): @classmethod def from_modifiers(cls, code, modifiers): code = code - exposure = 'on' if modifiers[0] == 1 else 'off' + exposure = 'on' if modifiers[0].value == 1 else 'off' width = modifiers[1] start_x = modifiers[2] start_y = modifiers[3] @@ -141,7 +149,7 @@ class AMCenterLinePrimitiveDef(AMPrimitiveDef): @classmethod def from_modifiers(cls, code, modifiers): code = code - exposure = 'on' if modifiers[0] == 1 else 'off' + exposure = 'on' if modifiers[0].value == 1 else 'off' width = modifiers[1] height = modifiers[2] x = modifiers[3] @@ -191,7 +199,7 @@ class AMOutlinePrimitiveDef(AMPrimitiveDef): def from_modifiers(cls, code, modifiers): num_points = modifiers[1] + 1 code = code - exposure = 'on' if modifiers[0] == 1 else 'off' + exposure = 'on' if modifiers[0].value == 1 else 'off' addrs = modifiers[2:num_points * 2] rotation = modifiers[3 + num_points * 2] return cls(code, exposure, addrs, rotation) @@ -231,7 +239,7 @@ class AMPolygonPrimitiveDef(AMPrimitiveDef): @classmethod def from_modifiers(cls, code, modifiers): code = code - exposure = 'on' if modifiers[0] == 1 else 'off' + exposure = 'on' if modifiers[0].value == 1 else 'off' vertices = modifiers[1] x = modifiers[2] y = modifiers[3] @@ -417,6 +425,9 @@ class AMVariableDef(object): yield i yield (OpCode.STORE, self.number) + def rotate(self, angle, center=None): + pass + def to_primitive_defs(instructions): classes = { 0: AMCommentPrimitiveDef, @@ -434,4 +445,4 @@ def to_primitive_defs(instructions): yield AMVariableDef(-code, modifiers[0]) else: primitive = classes[code] - yield primitive.from_modifiers(code, modifiers) \ No newline at end of file + yield primitive.from_modifiers(code, modifiers) diff --git a/gerberex/composition.py b/gerberex/composition.py index 2613a61..afcaf97 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -101,7 +101,7 @@ class GerberComposition(Composition): if not self.settings: self.settings = file.settings - self.param_statements = file.header + self.param_statements = [file.header] def _register_aperture_macro(self, statement): diff --git a/gerberex/dxf.py b/gerberex/dxf.py index b641924..11072b5 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -111,7 +111,7 @@ class DxfArcStatement(DxfStatement): settings.zero_suppression), write_gerber_value(begin_y, settings.format, settings.zero_suppression), - '03' if deg0 > deg1 else '02', + '03', write_gerber_value(end_x, settings.format, settings.zero_suppression), write_gerber_value(end_y, settings.format, diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 78e6e5f..90d6742 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -6,6 +6,7 @@ from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) from gerber.excellon_statements import UnitStmt from gerber.cam import FileSettings +from gerberex.utility import rotate def loads(data, filename=None, settings=None, tools=None, format=None): if not settings: @@ -27,6 +28,12 @@ class ExcellonFileEx(ExcellonFile): def __init__(self, statements, tools, hits, settings, filename=None): super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename) + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + for hit in self.hits: + hit.position = rotate(hit.position[0], hit.position[1], angle, center) + class UnitStmtEx(UnitStmt): @classmethod def from_statement(cls, stmt): diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py index e9d82cd..4b477d3 100644 --- a/gerberex/rs274x.py +++ b/gerberex/rs274x.py @@ -4,7 +4,9 @@ # Copyright 2019 Hiroshi Murayama import gerber.rs274x -from gerberex.statements import (AMParamStmt, AMParamStmtEx) +from gerber.gerber_statements import ADParamStmt, CoordStmt +from gerberex.statements import AMParamStmt, AMParamStmtEx +from gerberex.utility import rotate class GerberFile(gerber.rs274x.GerberFile): @classmethod @@ -23,3 +25,64 @@ class GerberFile(gerber.rs274x.GerberFile): def __init__(self, statements, settings, primitives, apertures, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename) + + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + self._generalize_aperture() + for statement in self.statements: + if isinstance(statement, AMParamStmtEx): + statement.rotate(angle, center) + elif isinstance(statement, CoordStmt) and statement.x != None and statement.y != None: + statement.x, statement.y = rotate(statement.x, statement.y, angle, center) + + def _generalize_aperture(self): + RECTANGLE = 0 + LANDSCAPE_OBROUND = 1 + PORTRATE_OBROUND = 2 + POLYGON = 3 + macro_defs = [ + ('MACR', AMParamStmtEx.rectangle), + ('MACLO', AMParamStmtEx.landscape_obround), + ('MACPO', AMParamStmtEx.portrate_obround), + ('MACP', AMParamStmtEx.polygon) + ] + + need_to_change = False + insert_point = 0 + last_aperture = 0 + macros = {} + for idx in range(0, len(self.statements)): + statement = self.statements[idx] + if isinstance(statement, AMParamStmtEx): + macros[statement.name] = statement + if not need_to_change: + insert_point = idx + 1 + if isinstance(statement, ADParamStmt) and statement.shape in ['R', 'O', 'P']: + need_to_change = True + last_aperture = idx + + if need_to_change: + for idx in range(0, len(macro_defs)): + macro_def = macro_defs[idx] + name = macro_def[0] + num = 1 + while name in macros: + name = '%s_%d' % (macro_def[0], num) + num += 1 + self.statements.insert(insert_point, macro_def[1](name)) + macro_defs[idx] = (name, macro_def[1]) + for idx in range(insert_point, last_aperture + len(macro_defs) + 1): + statement = self.statements[idx] + if isinstance(statement, ADParamStmt): + if statement.shape == 'R': + statement.shape = macro_defs[RECTANGLE][0] + elif statement.shape == 'O': + x = statement.modifiers[0] \ + if len(statement.modifiers) > 0 else 0 + y = statement.modifiers[1] \ + if len(statement.modifiers) > 1 else 0 + statement.shape = macro_defs[LANDSCAPE_OBROUND][0] \ + if x > y else macro_defs[PORTRATE_OBROUND][0] + elif statement.shape == 'P': + statement.shape = macro_defs[POLYGON][0] diff --git a/gerberex/statements.py b/gerberex/statements.py index 77ca235..c41acb9 100644 --- a/gerberex/statements.py +++ b/gerberex/statements.py @@ -11,9 +11,41 @@ class AMParamStmtEx(AMParamStmt): def from_stmt(cls, stmt): return cls(stmt.param, stmt.name, stmt.macro) + @classmethod + def circle(cls, name): + return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0') + + @classmethod + def rectangle(cls, name): + return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0') + + @classmethod + def landscape_obround(cls, name): + return cls( + 'AM', name, + '$4=$1-$2*' + '21,1,$1-$4,$2,0,0,0*' + '1,1,$4,$4/2,0,0*' + '1,1,$4,-$4/2,0,0*' + '1,0,$3,0,0,0') + + @classmethod + def portrate_obround(cls, name): + return cls( + 'AM', name, + '$4=$2-$1*' + '21,1,$1,$2-$4,0,0,0*' + '1,1,$4,0,$4/2,0*' + '1,1,$4,0,-$4/2,0*' + '1,0,$3,0,0,0') + + @classmethod + def polygon(cls, name): + return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0') + def __init__(self, param, name, macro): super(AMParamStmtEx, self).__init__(param, name, macro) - self.primitive_defs = to_primitive_defs(self.instructions) + self.primitive_defs = list(to_primitive_defs(self.instructions)) def to_inch(self): if self.units == 'metric': @@ -32,3 +64,7 @@ class AMParamStmtEx(AMParamStmt): for p in self.primitive_defs: yield p.to_gerber(settings) return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist())) + + def rotate(self, angle, center=None): + for primitive_def in self.primitive_defs: + primitive_def.rotate(angle, center) diff --git a/gerberex/utility.py b/gerberex/utility.py new file mode 100644 index 0000000..852519a --- /dev/null +++ b/gerberex/utility.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from math import cos, sin, pi + +def rotate(x, y, angle, center): + x0 = x - center[0] + y0 = y - center[1] + angle = angle * pi / 180.0 + return (cos(angle) * x0 - sin(angle) * y0 + center[0], + sin(angle) * x0 + cos(angle) * y0 + center[1]) -- cgit From 7e8f90b3724c62f72cf58ad28c11506212f3b706 Mon Sep 17 00:00:00 2001 From: opiopan Date: Mon, 25 Mar 2019 20:21:41 +0900 Subject: fix a setup issue --- gerberex/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'gerberex') diff --git a/gerberex/__init__.py b/gerberex/__init__.py index 388f592..cef11ab 100644 --- a/gerberex/__init__.py +++ b/gerberex/__init__.py @@ -2,6 +2,13 @@ # -*- coding: utf-8 -*- # Copyright 2019 Hiroshi Murayama +""" +Gerber Tools Extension +====================== +**Gerber Tools Extenstion** +gerber-tools-extension is a extention package for gerber-tools. +This package provide panelizing of PCB fucntion. +""" from gerberex.common import (read, loads) from gerberex.composition import (GerberComposition, DrillComposition) -- cgit From fcd704e1eef9034e2000f55b2918d7df41379408 Mon Sep 17 00:00:00 2001 From: opiopan Date: Sat, 30 Mar 2019 11:16:13 +0900 Subject: add mouse bites generator function --- gerberex/composition.py | 22 ++++++ gerberex/dxf.py | 205 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 184 insertions(+), 43 deletions(-) (limited to 'gerberex') diff --git a/gerberex/composition.py b/gerberex/composition.py index afcaf97..73f5702 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -127,10 +127,13 @@ class DrillComposition(Composition): self.header2_statements = [] self.tools = [] self.hits = [] + self.dxf_statements = [] def merge(self, file): if isinstance(file, gerberex.excellon.ExcellonFileEx): self._merge_excellon(file) + elif isinstance(file, gerberex.DxfFile): + self._merge_dxf(file) else: raise Exception('unsupported file type') @@ -147,6 +150,9 @@ class DrillComposition(Composition): for h in self.hits: if h.tool.number == t.number: yield CoordinateStmt(*h.position).to_excellon(self.settings) + for num, statement in self.dxf_statements: + if num == t.number: + yield statement.to_excellon(self.settings) yield EndOfProgramStmt().to_excellon() with open(path, 'w') as f: @@ -186,6 +192,22 @@ class DrillComposition(Composition): hit.tool = tool_map[hit.tool.number] self.hits.append(hit) + def _merge_dxf(self, file): + 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: + self.header1_statements = file.header + self.header2_statements = file.header2 + + tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width)) + self.dxf_statements.append((tool.number, file.statements)) + def _register_tool(self, tool): for existing in self.tools: if existing.equivalent(tool): diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 11072b5..a2c26d0 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -8,13 +8,18 @@ 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 +from gerber.excellon_statements import ExcellonTool +from gerber.excellon_statements import CoordinateStmt import dxfgrabber class DxfStatement(object): def __init__(self, entity): self.entity = entity - def to_gerber(self, settings=None): + def to_gerber(self, settings=None, pitch=0, width=0): + pass + + def to_excellon(self, settings=None, pitch=0, width=0): pass def to_inch(self): @@ -27,39 +32,77 @@ 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_gerber(self, settings=FileSettings(), pitch=0, width=0): + if pitch == 0: + 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) + ) + else: + gstr = "" + for p in self._dots(pitch, width): + gstr += 'X{0}Y{1}D03*\n'.format( + write_gerber_value(p[0], settings.format, + settings.zero_suppression), + write_gerber_value(p[1], settings.format, + settings.zero_suppression)) + return gstr + + def to_excellon(self, settings=FileSettings(), pitch=0, width=0): + if not pitch: + return + gstr = "" + for p in self._dots(pitch, width): + gstr += CoordinateStmt(x=p[0], y=p[1]).to_excellon(settings) + '\n' + return gstr 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])) + self.entity.start = ( + inch(self.entity.start[0]), inch(self.entity.start[1])) + self.entity.end = ( + inch(self.entity.end[0]), inch(self.entity.end[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])) + self.entity.start = ( + metric(self.entity.start[0]), inch(self.entity.start[1])) + self.entity.end = ( + metric(self.entity.end[0]), inch(self.entity.end[1])) + + def _dots(self, pitch, width): + x0 = self.entity.start[0] + y0 = self.entity.start[1] + x1 = self.entity.end[0] + y1 = self.entity.end[1] + xp = x1 - x0 + yp = y1 - y0 + l = sqrt(xp * xp + yp * yp) + xd = xp * pitch / l + yd = yp * pitch / l + + d = 0; + while d < l + width / 2: + yield (x0, y0) + x0 += xd + y0 += yd + d += pitch class DxfCircleStatement(DxfStatement): def __init__(self, entity): super(DxfCircleStatement, self).__init__(entity) - def to_gerber(self, settings=FileSettings): + def to_gerber(self, settings=FileSettings(), pitch=0, width=0): + if pitch: + return r = self.entity.radius x0 = self.entity.center[0] y0 = self.entity.center[1] @@ -82,19 +125,21 @@ class DxfCircleStatement(DxfStatement): 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])) + self.entity.center = ( + inch(self.entity.center[0]), inch(self.entity.center[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])) + self.entity.center = ( + metric(self.entity.center[0]), metric(self.entity.center[1])) class DxfArcStatement(DxfStatement): def __init__(self, entity): super(DxfArcStatement, self).__init__(entity) - def to_gerber(self, settings=FileSettings): + def to_gerber(self, settings=FileSettings(), pitch=0, width=0): + if pitch: + return deg0 = self.entity.start_angle deg1 = self.entity.end_angle r = self.entity.radius @@ -126,21 +171,23 @@ class DxfArcStatement(DxfStatement): 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])) + self.entity.center = ( + inch(self.entity.center[0]), inch(self.entity.center[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])) + self.entity.center = ( + metric(self.entity.center[0]), metric(self.entity.center[1])) class DxfPolylineStatement(DxfStatement): def __init__(self, entity): super(DxfPolylineStatement, self).__init__(entity) - def to_gerber(self, settings=FileSettings()): + def to_gerber(self, settings=FileSettings(), pitch=0, width=0): + if pitch: + return x0 = self.entity.points[0][0] y0 = self.entity.points[0][1] b = self.entity.bulge[0] @@ -214,6 +261,8 @@ class DxfStatements(object): self._units = units self.dcode = dcode self.draw_mode = draw_mode + self.pitch = inch(1) if self._units == 'unit' else 1 + self.width = 0 self.statements = [] for entity in entities: if entity.dxftype == 'LWPOLYLINE': @@ -241,19 +290,33 @@ class DxfStatements(object): yield 'G37*' else: for statement in self.statements: - yield statement.to_gerber(settings) + yield statement.to_gerber( + settings, + pitch=self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0, + width=self.width) return '\n'.join(gerbers()) + def to_excellon(self, settings=FileSettings()): + if not self.draw_mode == DxfFile.DM_MOUSE_BITES: + return + def drills(): + for statement in self.statements: + if isinstance(statement, DxfLineStatement): + yield statement.to_excellon(settings, pitch=self.pitch, width=self.width) + return '\n'.join(drills()) + def to_inch(self): if self._units == 'metric': self._units = 'inch' + self.pitch = inch(self.pitch) for statement in self.statements: statement.to_inch() def to_metric(self): if self._units == 'inch': self._units = 'metric' + self.pitch = metric(self.pitch) for statement in self.statements: statement.to_metric() @@ -270,6 +333,32 @@ class DxfHeaderStatement(object): settings.format[0], settings.format[1], settings.format[0], settings.format[1] ) + + def to_excellon(self, settings): + return 'M48\n'\ + 'FMAT,2\n'\ + 'ICI,{0}\n'\ + '{1},{2},{3}.{4}\n'\ + '{5}'.format( + 'ON' if settings.notation == 'incremental' else 'OFF', + 'INCH' if settings.units == 'inch' else 'METRIC', + 'TZ' if settings.zero_suppression == 'leading' else 'LZ', + '0' * settings.format[0], '0' * settings.format[1], + 'M72' if settings.units == 'inch' else 'M71' + ) + + def to_inch(self): + pass + + def to_metric(self): + pass + +class DxfHeader2Statement(object): + def to_gerber(self, settings): + pass + + def to_excellon(self, settings): + return '%' def to_inch(self): pass @@ -280,8 +369,15 @@ class DxfHeaderStatement(object): class DxfFile(CamFile): DM_LINE = 0 DM_FILL = 1 + DM_MOUSE_BITES = 2 + + FT_RX274X = 0 + FT_EXCELLON = 1 + + def __init__(self, dxf, settings=None, draw_mode=None, filename=None): + if not settings: + settings = FileSettings(zero_suppression='leading') - 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: @@ -294,6 +390,8 @@ class DxfFile(CamFile): super(DxfFile, self).__init__(settings=settings, filename=filename) self._draw_mode = draw_mode self.header = DxfHeaderStatement() + + self.header2 = DxfHeader2Statement() 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) @@ -313,6 +411,7 @@ class DxfFile(CamFile): @width.setter def width(self, value): self.aperture.modifiers = ([float(value),],) + self.statements.width = value @property def draw_mode(self): @@ -322,23 +421,42 @@ class DxfFile(CamFile): def draw_mode(self, value): self._draw_mode = value self.statements.draw_mode = value + + @property + def pitch(self): + return self.statements.pitch - def write(self, filename=None): + @pitch.setter + def pitch(self, value): + self.statements.pitch = value + + def write(self, filename=None, filetype=FT_RX274X): 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') + if filetype == self.FT_RX274X: + 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') + else: + tool = ExcellonTool(self.settings, number=1, diameter=self.width) + f.write(self.header.to_excellon(self.settings) + '\n') + f.write(tool.to_excellon(self.settings) + '\n') + f.write(self.header2.to_excellon(self.settings) + '\n') + f.write('T01\n') + f.write(self.statements.to_excellon(self.settings) + '\n') + f.write('M30\n') + def to_inch(self): if self.units == 'metric': self.header.to_inch() self.aperture.to_inch() self.statements.to_inch() + self.pitch = inch(self.pitch) self.units = 'inch' def to_metric(self): @@ -346,6 +464,7 @@ class DxfFile(CamFile): self.header.to_metric() self.aperture.to_metric() self.statements.to_metric() + self.pitch = metric(self.pitch) self.units = 'metric' def offset(self, ofset_x, offset_y): -- cgit From 900d992fa3af05f93ac7a4cf717f28598e1a868d Mon Sep 17 00:00:00 2001 From: opiopan Date: Sun, 31 Mar 2019 13:30:15 +0900 Subject: auto detection closed paths in the collection of DXF arc object and line object, then fill these closed path --- gerberex/dxf.py | 198 +++++++++++++++++++++++++++++++++++++-------------- gerberex/dxf_path.py | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+), 53 deletions(-) create mode 100644 gerberex/dxf_path.py (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index a2c26d0..e6eb971 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -5,16 +5,29 @@ import io from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt +import dxfgrabber from gerber.cam import CamFile, FileSettings from gerber.utils import inch, metric, write_gerber_value from gerber.gerber_statements import ADParamStmt from gerber.excellon_statements import ExcellonTool from gerber.excellon_statements import CoordinateStmt -import dxfgrabber +from gerberex.dxf_path import generate_closed_paths + +ACCEPTABLE_ERROR = 0.001 + +def is_equal_value(a, b, error_range=0): + return a - b <= error_range and a - b >= -error_range + +def is_equal_point(a, b, error_range=0): + return is_equal_value(a[0], b[0], error_range) and \ + is_equal_value(a[1], b[1], error_range) class DxfStatement(object): def __init__(self, entity): self.entity = entity + self.start = None + self.end = None + self.is_closed = False def to_gerber(self, settings=None, pitch=0, width=0): pass @@ -28,16 +41,22 @@ class DxfStatement(object): def to_metric(self): pass + def is_equal_to(self, target, error_range=0): + return False + + def reverse(self): + raise Exception('Not implemented') + class DxfLineStatement(DxfStatement): def __init__(self, entity): super(DxfLineStatement, self).__init__(entity) + self.start = (self.entity.start[0], self.entity.start[1]) + self.end = (self.entity.end[0], self.entity.end[1]) def to_gerber(self, settings=FileSettings(), pitch=0, width=0): if pitch == 0: - x0 = self.entity.start[0] - y0 = self.entity.start[1] - x1 = self.entity.end[0] - y1 = self.entity.end[1] + x0, y0 = self.start + x1, y1 = self.end return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format( write_gerber_value(x0, settings.format, settings.zero_suppression), @@ -67,22 +86,34 @@ class DxfLineStatement(DxfStatement): return gstr def to_inch(self): - self.entity.start = ( - inch(self.entity.start[0]), inch(self.entity.start[1])) - self.entity.end = ( - inch(self.entity.end[0]), inch(self.entity.end[1])) + self.start = ( + inch(self.start[0]), inch(self.start[1])) + self.end = ( + inch(self.end[0]), inch(self.end[1])) def to_metric(self): - self.entity.start = ( - metric(self.entity.start[0]), inch(self.entity.start[1])) - self.entity.end = ( - metric(self.entity.end[0]), inch(self.entity.end[1])) + self.start = ( + metric(self.start[0]), inch(self.start[1])) + self.end = ( + metric(self.end[0]), inch(self.end[1])) + + def is_equal_to(self, target, error_range=0): + if not isinstance(target, DxfLineStatement): + return False + return (is_equal_point(self.start, target.start, error_range) and \ + is_equal_point(self.end, target.end, error_range)) or \ + (is_equal_point(self.start, target.end, error_range) and \ + is_equal_point(self.end, target.start, error_range)) + + def reverse(self): + pt = self.start + self.start = self.end + self.end = pt def _dots(self, pitch, width): - x0 = self.entity.start[0] - y0 = self.entity.start[1] - x1 = self.entity.end[0] - y1 = self.entity.end[1] + x0, y0 = self.start + x1, y1 = self.end + y1 = self.end[1] xp = x1 - x0 yp = y1 - y0 l = sqrt(xp * xp + yp * yp) @@ -99,13 +130,17 @@ class DxfLineStatement(DxfStatement): class DxfCircleStatement(DxfStatement): def __init__(self, entity): super(DxfCircleStatement, self).__init__(entity) + self.radius = self.entity.radius + self.center = (self.entity.center[0], self.entity.center[1]) + self.start = (self.center[0] + self.radius, self.center[1]) + self.end = self.start + self.is_closed = True def to_gerber(self, settings=FileSettings(), pitch=0, width=0): if pitch: return - r = self.entity.radius - x0 = self.entity.center[0] - y0 = self.entity.center[1] + r = self.radius + x0, y0 = self.center 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, @@ -124,66 +159,107 @@ class DxfCircleStatement(DxfStatement): ) def to_inch(self): - self.entity.radius = inch(self.entity.radius) - self.entity.center = ( - inch(self.entity.center[0]), inch(self.entity.center[1])) + self.radius = inch(self.radius) + self.center = ( + inch(self.center[0]), inch(self.center[1])) def to_metric(self): - self.entity.radius = metric(self.entity.radius) - self.entity.center = ( - metric(self.entity.center[0]), metric(self.entity.center[1])) + self.radius = metric(self.radius) + self.center = ( + metric(self.center[0]), metric(self.center[1])) + + def is_equal_to(self, target, error_range=0): + if not isinstance(target, DxfCircleStatement): + return False + return is_equal_point(self.center, target.enter, error_range) and \ + is_equal_value(self.radius, target.radius) + + def reverse(self): + pass class DxfArcStatement(DxfStatement): def __init__(self, entity): super(DxfArcStatement, self).__init__(entity) + self.start_angle = self.entity.start_angle + self.end_angle = self.entity.end_angle + self.radius = self.entity.radius + self.center = (self.entity.center[0], self.entity.center[1]) + self.start = ( + self.center[0] + self.radius * cos(self.start_angle / 180. * pi), + self.center[1] + self.radius * sin(self.start_angle / 180. * pi), + ) + self.end = ( + self.center[0] + self.radius * cos(self.end_angle / 180. * pi), + self.center[1] + self.radius * sin(self.end_angle / 180. * pi), + ) + angle = self.end_angle - self.start_angle + self.is_closed = angle >= 360 or angle <= -360 def to_gerber(self, settings=FileSettings(), pitch=0, width=0): if pitch: return - 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) + x0 = self.center[0] + y0 = self.center[1] + start_x, start_y = self.start + end_x, end_y = self.end 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, + write_gerber_value(start_x, settings.format, settings.zero_suppression), - write_gerber_value(begin_y, settings.format, + write_gerber_value(start_y, settings.format, settings.zero_suppression), - '03', + '02' if self.start_angle > self.end_angle else '03', 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, + write_gerber_value(x0 - start_x, settings.format, settings.zero_suppression), - write_gerber_value(y0 - begin_y, settings.format, + write_gerber_value(y0 - start_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 = ( - inch(self.entity.center[0]), inch(self.entity.center[1])) + self.radius = inch(self.radius) + self.center = (inch(self.center[0]), inch(self.center[1])) + self.start = (inch(self.start[0]), inch(self.start[1])) + self.end = (inch(self.end[0]), inch(self.end[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 = ( - metric(self.entity.center[0]), metric(self.entity.center[1])) + self.radius = metric(self.radius) + self.center = (metric(self.center[0]), metric(self.center[1])) + self.start = (metric(self.start[0]), metric(self.start[1])) + self.end = (metric(self.end[0]), metric(self.end[1])) + + def is_equal_to(self, target, error_range=0): + if not isinstance(target, DxfArcStatement): + return False + aerror_range = error_range / pi * self.radius * 180 + return is_equal_point(self.center, target.center, error_range) and \ + is_equal_value(self.radius, target.radius, error_range) and \ + ((is_equal_value(self.start_angle, target.start_angle, aerror_range) and + is_equal_value(self.end_angle, target.end_angle, aerror_range)) or + (is_equal_value(self.start_angle, target.end_angle, aerror_range) and + is_equal_value(self.end_angle, target.end_angle, aerror_range))) + + def reverse(self): + tmp = self.start_angle + self.start_angle = self.end_angle + self.end_angle = tmp + tmp = self.start + self.start = self.end + self.end = tmp class DxfPolylineStatement(DxfStatement): def __init__(self, entity): super(DxfPolylineStatement, self).__init__(entity) + self.start = (self.entity.points[0][0], self.entity.points[0][1]) + self.is_closed = self.entity.is_closed + if self.is_closed: + self.end = self.start + else: + self.start = (self.entity.points[-1][0], self.entity.points[-1][1]) def to_gerber(self, settings=FileSettings(), pitch=0, width=0): if pitch: @@ -243,12 +319,16 @@ class DxfPolylineStatement(DxfStatement): return gerber def to_inch(self): + self.start = (inch(self.start[0]), inch(self.start[1])) + self.end = (inch(self.end[0]), inch(self.end[1])) 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): + self.start = (metric(self.start[0]), metric(self.start[1])) + self.end = (metric(self.end[0]), metric(self.end[1])) 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])) @@ -261,8 +341,9 @@ class DxfStatements(object): self._units = units self.dcode = dcode self.draw_mode = draw_mode - self.pitch = inch(1) if self._units == 'unit' else 1 + self.pitch = inch(1) if self._units == 'inch' else 1 self.width = 0 + self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR self.statements = [] for entity in entities: if entity.dxftype == 'LWPOLYLINE': @@ -273,6 +354,7 @@ class DxfStatements(object): self.statements.append(DxfCircleStatement(entity)) elif entity.dxftype == 'ARC': self.statements.append(DxfArcStatement(entity)) + self.paths = generate_closed_paths(self.statements, self.error_range) @property def units(self): @@ -282,12 +364,16 @@ class DxfStatements(object): def gerbers(): yield 'D{0}*'.format(self.dcode) if self.draw_mode == DxfFile.DM_FILL: - yield 'G36*' for statement in self.statements: + yield 'G36*' if isinstance(statement, DxfCircleStatement) or \ (isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed): yield statement.to_gerber(settings) - yield 'G37*' + yield 'G37*' + for path in self.paths: + yield 'G36*' + yield path.to_gerber(settings) + yield 'G37*' else: for statement in self.statements: yield statement.to_gerber( @@ -310,15 +396,21 @@ class DxfStatements(object): if self._units == 'metric': self._units = 'inch' self.pitch = inch(self.pitch) + self.error_range = inch(self.error_range) for statement in self.statements: statement.to_inch() + for path in self.paths: + path.to_inch() def to_metric(self): if self._units == 'inch': self._units = 'metric' self.pitch = metric(self.pitch) + self.error_range = metric(self.error_range) for statement in self.statements: statement.to_metric() + for path in self.paths: + path.to_metric() class DxfHeaderStatement(object): def to_gerber(self, settings): diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py new file mode 100644 index 0000000..b066b08 --- /dev/null +++ b/gerberex/dxf_path.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from gerber.utils import inch, metric, write_gerber_value +from gerber.cam import FileSettings + +class DxfPath(object): + def __init__(self, statement, error_range=0): + self.statements = [statement] + self.error_range = error_range + + @property + def start(self): + return self.statements[0].start + + @property + def end(self): + return self.statements[-1].end + + @property + def is_closed(self): + from gerberex.dxf import is_equal_point + return len(self.statements) > 1 and \ + is_equal_point(self.start, self.end, self.error_range) + + def is_equal_to(self, target, error_range=0): + from gerberex.dxf import is_equal_point + if not isinstance(target, DxfPath): + return False + if len(self.statements) != len(target.statements): + return False + if is_equal_point(self.start, target.start, error_range) and \ + is_equal_point(self.end, target.end, error_range): + for i in range(0, len(self.statements)): + if not self.statements[i].is_equal_to(target.statements[i], error_range): + return False + return True + elif is_equal_point(self.start, target.end, error_range) and \ + is_equal_point(self.end, target.start, error_range): + for i in range(0, len(self.statements)): + if not self.statements[i].is_equal_to(target.statements[-1 - i], error_range): + return False + return True + return False + + def to_inch(self): + self.error_range = inch(self.error_range) + + def to_metric(self): + self.error_range = metric(self.error_range) + + def reverse(self): + rlist = [] + for statement in reversed(self.statements): + statement.reverse() + rlist.append(statement) + self.statements = rlist + + def merge(self, element, error_range=0): + from gerberex.dxf import is_equal_point + if self.is_closed or element.is_closed: + return False + if not error_range: + error_range = self.error_range + if is_equal_point(self.end, element.start, error_range): + return self._append_at_end(element, error_range) + elif is_equal_point(self.end, element.end, error_range): + element.reverse() + return self._append_at_end(element, error_range) + elif is_equal_point(self.start, element.end, error_range): + return self._insert_on_top(element, error_range) + elif is_equal_point(self.start, element.start, error_range): + element.reverse() + return self._insert_on_top(element, error_range) + else: + return False + + def _append_at_end(self, element, error_range=0): + if isinstance(element, DxfPath): + if self.is_equal_to(element, error_range): + return False + for i in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[-1 - i].is_equal_to(element.statements[i]): + break + for j in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[j].is_equal_to(element.statements[-1 - j]): + break + if i + j >= len(element.statements): + return False + mergee = list(element.statements) + if i > 0: + del mergee[0:i] + del self.statements[-i] + if j > 0: + del mergee[-j] + del self.statements[0:j] + self.statements.extend(mergee) + return True + else: + if self.statements[-1].is_equal_to(element, error_range) or \ + self.statements[0].is_equal_to(element, error_range): + return False + self.statements.appen(element) + return True + + def _insert_on_top(self, element, error_range=0): + if isinstance(element, DxfPath): + if self.is_equal_to(element, error_range): + return False + for i in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[-1 - i].is_equal_to(element.statements[i]): + break + for j in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[j].is_equal_to(element.statements[-1 - j]): + break + if i + j >= len(element.statements): + return False + mergee = list(element.statements) + if i > 0: + del mergee[0:i] + del self.statements[-i] + if j > 0: + del mergee[-j] + del self.statements[0:j] + self.statements[0:0] = mergee + return True + else: + if self.statements[-1].is_equal_to(element, error_range) or \ + self.statements[0].is_equal_to(element, error_range): + return False + self.statements.insert(0, element) + return True + + def to_gerber(self, settings=FileSettings(), pitch=0, width=0): + from gerberex.dxf import DxfArcStatement + if pitch: + return + + x0, y0 = self.statements[0].start + 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), + ) + + for statement in self.statements: + x0, y0 = statement.start + x1, y1 = statement.end + if isinstance(statement, DxfArcStatement): + xc, yc = statement.center + gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format( + '03' if statement.end_angle > statement.start_angle 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) + ) + else: + 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), + ) + + return gerber + +def generate_closed_paths(statements, error_range=0): + from gerberex.dxf import DxfLineStatement, DxfArcStatement + + paths = [DxfPath(s, error_range) \ + for s in filter(lambda s: isinstance(s, DxfLineStatement) or \ + isinstance(s, DxfArcStatement), statements)] + + prev_paths_num = 0 + while prev_paths_num != len(paths): + working = [] + for i in range(len(paths)): + mergee = paths[i] + for j in range(i + 1, len(paths) - 1): + target = paths[j] + if target.merge(mergee, error_range): + break + else: + working.append(mergee) + prev_paths_num = len(paths) + paths = working + + return list(filter(lambda p: p.is_closed, paths)) + -- cgit From 53816574a986722d7af26c5597248d9c96f31bd3 Mon Sep 17 00:00:00 2001 From: opiopan Date: Sun, 31 Mar 2019 18:16:34 +0900 Subject: fix a minor issue --- gerberex/dxf.py | 8 +++----- gerberex/dxf_path.py | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index e6eb971..e863238 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -259,7 +259,7 @@ class DxfPolylineStatement(DxfStatement): if self.is_closed: self.end = self.start else: - self.start = (self.entity.points[-1][0], self.entity.points[-1][1]) + self.end = (self.entity.points[-1][0], self.entity.points[-1][1]) def to_gerber(self, settings=FileSettings(), pitch=0, width=0): if pitch: @@ -364,16 +364,14 @@ class DxfStatements(object): def gerbers(): yield 'D{0}*'.format(self.dcode) if self.draw_mode == DxfFile.DM_FILL: + yield 'G36*' for statement in self.statements: - yield 'G36*' if isinstance(statement, DxfCircleStatement) or \ (isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed): yield statement.to_gerber(settings) - yield 'G37*' for path in self.paths: - yield 'G36*' yield path.to_gerber(settings) - yield 'G37*' + yield 'G37*' else: for statement in self.statements: yield statement.to_gerber( diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py index b066b08..930d8d2 100644 --- a/gerberex/dxf_path.py +++ b/gerberex/dxf_path.py @@ -184,7 +184,7 @@ def generate_closed_paths(statements, error_range=0): working = [] for i in range(len(paths)): mergee = paths[i] - for j in range(i + 1, len(paths) - 1): + for j in range(i + 1, len(paths)): target = paths[j] if target.merge(mergee, error_range): break @@ -192,6 +192,5 @@ def generate_closed_paths(statements, error_range=0): working.append(mergee) prev_paths_num = len(paths) paths = working - return list(filter(lambda p: p.is_closed, paths)) -- cgit From eda75275505e14439e2dcd1990d2b95217546db1 Mon Sep 17 00:00:00 2001 From: opiopan Date: Mon, 1 Apr 2019 22:07:56 +0900 Subject: compliant with Python 2.7 --- gerberex/dxf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index e863238..1b2ddf7 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -3,7 +3,7 @@ # Copyright 2019 Hiroshi Murayama -import io +import io, sys from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt import dxfgrabber from gerber.cam import CamFile, FileSettings @@ -561,6 +561,8 @@ class DxfFile(CamFile): raise Exception('Not supported') def loads(data, filename=None): + if sys.version_info.major == 2: + data = unicode(data) stream = io.StringIO(data) dxf = dxfgrabber.read(stream) return DxfFile(dxf) -- cgit From cb420e39e278f7ab6f002600a7698d7be101eb7d Mon Sep 17 00:00:00 2001 From: opiopan Date: Wed, 3 Apr 2019 00:30:00 +0900 Subject: fix a rotaion issue --- gerberex/dxf.py | 8 +------- gerberex/dxf_path.py | 4 +--- gerberex/rs274x.py | 16 +++++++++++++++- gerberex/utility.py | 8 ++++++++ 4 files changed, 25 insertions(+), 11 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 1b2ddf7..39d256b 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -11,17 +11,11 @@ from gerber.utils import inch, metric, write_gerber_value from gerber.gerber_statements import ADParamStmt from gerber.excellon_statements import ExcellonTool from gerber.excellon_statements import CoordinateStmt +from gerberex.utility import is_equal_point, is_equal_value from gerberex.dxf_path import generate_closed_paths ACCEPTABLE_ERROR = 0.001 -def is_equal_value(a, b, error_range=0): - return a - b <= error_range and a - b >= -error_range - -def is_equal_point(a, b, error_range=0): - return is_equal_value(a[0], b[0], error_range) and \ - is_equal_value(a[1], b[1], error_range) - class DxfStatement(object): def __init__(self, entity): self.entity = entity diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py index 930d8d2..2825c58 100644 --- a/gerberex/dxf_path.py +++ b/gerberex/dxf_path.py @@ -5,6 +5,7 @@ from gerber.utils import inch, metric, write_gerber_value from gerber.cam import FileSettings +from gerberex.utility import is_equal_point, is_equal_value class DxfPath(object): def __init__(self, statement, error_range=0): @@ -21,12 +22,10 @@ class DxfPath(object): @property def is_closed(self): - from gerberex.dxf import is_equal_point return len(self.statements) > 1 and \ is_equal_point(self.start, self.end, self.error_range) def is_equal_to(self, target, error_range=0): - from gerberex.dxf import is_equal_point if not isinstance(target, DxfPath): return False if len(self.statements) != len(target.statements): @@ -59,7 +58,6 @@ class DxfPath(object): self.statements = rlist def merge(self, element, error_range=0): - from gerberex.dxf import is_equal_point if self.is_closed or element.is_closed: return False if not error_range: diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py index 4b477d3..4eb317d 100644 --- a/gerberex/rs274x.py +++ b/gerberex/rs274x.py @@ -30,11 +30,25 @@ class GerberFile(gerber.rs274x.GerberFile): if angle % 360 == 0: return self._generalize_aperture() + last_x = 0 + last_y = 0 + last_rx = 0 + last_ry = 0 for statement in self.statements: if isinstance(statement, AMParamStmtEx): statement.rotate(angle, center) elif isinstance(statement, CoordStmt) and statement.x != None and statement.y != None: - statement.x, statement.y = rotate(statement.x, statement.y, angle, center) + if statement.i != None and statement.j != None: + cx = last_x + statement.i + cy = last_y + statement.j + cx, cy = rotate(cx, cy, angle, center) + statement.i = cx - last_rx + statement.j = cy - last_ry + last_x = statement.x + last_y = statement.y + last_rx, last_ry = rotate(statement.x, statement.y, angle, center) + statement.x = last_rx + statement.y = last_ry def _generalize_aperture(self): RECTANGLE = 0 diff --git a/gerberex/utility.py b/gerberex/utility.py index 852519a..f90df96 100644 --- a/gerberex/utility.py +++ b/gerberex/utility.py @@ -11,3 +11,11 @@ def rotate(x, y, angle, center): angle = angle * pi / 180.0 return (cos(angle) * x0 - sin(angle) * y0 + center[0], sin(angle) * x0 + cos(angle) * y0 + center[1]) + +def is_equal_value(a, b, error_range=0): + return a - b <= error_range and a - b >= -error_range + + +def is_equal_point(a, b, error_range=0): + return is_equal_value(a[0], b[0], error_range) and \ + is_equal_value(a[1], b[1], error_range) -- cgit From 6b4603af21839c94a33d3804c364ff1b809eb341 Mon Sep 17 00:00:00 2001 From: opiopan Date: Wed, 3 Apr 2019 12:17:59 +0900 Subject: add rectangle generator --- gerberex/__init__.py | 4 +-- gerberex/common.py | 5 ++++ gerberex/dxf.py | 82 ++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 64 insertions(+), 27 deletions(-) (limited to 'gerberex') diff --git a/gerberex/__init__.py b/gerberex/__init__.py index cef11ab..f379c1c 100644 --- a/gerberex/__init__.py +++ b/gerberex/__init__.py @@ -10,6 +10,6 @@ gerber-tools-extension is a extention package for gerber-tools. This package provide panelizing of PCB fucntion. """ -from gerberex.common import (read, loads) -from gerberex.composition import (GerberComposition, DrillComposition) +from gerberex.common import read, loads, rectangle +from gerberex.composition import GerberComposition, DrillComposition from gerberex.dxf import DxfFile diff --git a/gerberex/common.py b/gerberex/common.py index 4a85bd4..47bc6b3 100644 --- a/gerberex/common.py +++ b/gerberex/common.py @@ -33,3 +33,8 @@ def loads(data, filename=None, format=None): return ipc356.loads(data, filename=filename) else: raise ParseError('Unable to detect file format') + + +def rectangle(width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None): + return gerberex.dxf.DxfFile.rectangle( + width, height, left, bottom, units, draw_mode, filename) diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 39d256b..9a2186c 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -42,10 +42,16 @@ class DxfStatement(object): raise Exception('Not implemented') class DxfLineStatement(DxfStatement): - def __init__(self, entity): + @classmethod + def from_entity(cls, entity): + start = (entity.start[0], entity.start[1]) + end = (entity.end[0], entity.end[1]) + return cls(entity, start, end) + + def __init__(self, entity, start, end): super(DxfLineStatement, self).__init__(entity) - self.start = (self.entity.start[0], self.entity.start[1]) - self.end = (self.entity.end[0], self.entity.end[1]) + self.start = start + self.end = end def to_gerber(self, settings=FileSettings(), pitch=0, width=0): if pitch == 0: @@ -329,7 +335,7 @@ class DxfPolylineStatement(DxfStatement): self.entity.bulge[idx] = metric(self.entity.bulge[idx]) class DxfStatements(object): - def __init__(self, entities, units, dcode=10, draw_mode=None): + def __init__(self, statements, units, dcode=10, draw_mode=None): if draw_mode == None: draw_mode = DxfFile.DM_LINE self._units = units @@ -338,16 +344,7 @@ class DxfStatements(object): self.pitch = inch(1) if self._units == 'inch' else 1 self.width = 0 self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR - 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)) + self.statements = statements self.paths = generate_closed_paths(self.statements, self.error_range) @property @@ -458,18 +455,52 @@ class DxfFile(CamFile): FT_RX274X = 0 FT_EXCELLON = 1 - def __init__(self, dxf, settings=None, draw_mode=None, filename=None): - if not settings: - settings = FileSettings(zero_suppression='leading') + @classmethod + def from_dxf(cls, dxf, settings=None, draw_mode=None, filename=None): + fsettings = settings if settings else \ + FileSettings(zero_suppression='leading') - if draw_mode == None: - draw_mode = self.DM_LINE if dxf.header['$INSUNITS'] == 1: - settings.units = 'inch' - settings.format = (2, 5) + fsettings.units = 'inch' + if not settings: + fsettings.format = (2, 5) + else: + fsettings.units = 'metric' + if not settings: + fsettings.format = (3, 4) + + statements = [] + for entity in dxf.entities: + if entity.dxftype == 'LWPOLYLINE': + statements.append(DxfPolylineStatement(entity)) + elif entity.dxftype == 'LINE': + statements.append(DxfLineStatement.from_entity(entity)) + elif entity.dxftype == 'CIRCLE': + statements.append(DxfCircleStatement(entity)) + elif entity.dxftype == 'ARC': + statements.append(DxfArcStatement(entity)) + + return cls(statements, fsettings, draw_mode, filename) + + @classmethod + def rectangle(cls, width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None): + if units == 'metric': + settings = FileSettings(units=units, zero_suppression='leading', format=(3,4)) else: - settings.units = 'metric' - settings.format = (3, 4) + settings = FileSettings(units=units, zero_suppression='leading', format=(2,5)) + statements = [ + DxfLineStatement(None, (left, bottom), (left + width, bottom)), + DxfLineStatement(None, (left + width, bottom), (left + width, bottom + height)), + DxfLineStatement(None, (left + width, bottom + height), (left, bottom + height)), + DxfLineStatement(None, (left, bottom + height), (left, bottom)), + ] + return cls(statements, settings, draw_mode, filename) + + def __init__(self, statements, settings=None, draw_mode=None, filename=None): + if not settings: + settings = FileSettings(units='metric', format=(3,4), zero_suppression='leading') + if draw_mode == None: + draw_mode = self.DM_LINE super(DxfFile, self).__init__(settings=settings, filename=filename) self._draw_mode = draw_mode @@ -477,7 +508,8 @@ class DxfFile(CamFile): self.header2 = DxfHeader2Statement() 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) + self.statements = DxfStatements( + statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode) @property def dcode(self): @@ -559,4 +591,4 @@ def loads(data, filename=None): data = unicode(data) stream = io.StringIO(data) dxf = dxfgrabber.read(stream) - return DxfFile(dxf) + return DxfFile.from_dxf(dxf) -- cgit From d53293a609a83aa945af6864285b90d36bcbdd69 Mon Sep 17 00:00:00 2001 From: opiopan Date: Wed, 3 Apr 2019 19:05:20 +0900 Subject: add move and rotation capability to DxfFile object --- gerberex/dxf.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 7 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 9a2186c..ba02f08 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -7,7 +7,7 @@ import io, sys from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt import dxfgrabber from gerber.cam import CamFile, FileSettings -from gerber.utils import inch, metric, write_gerber_value +from gerber.utils import inch, metric, write_gerber_value, rotate_point from gerber.gerber_statements import ADParamStmt from gerber.excellon_statements import ExcellonTool from gerber.excellon_statements import CoordinateStmt @@ -41,6 +41,13 @@ class DxfStatement(object): def reverse(self): raise Exception('Not implemented') + def offset(self, offset_x, offset_y): + raise Exception('Not supported') + + def rotate(self, angle, center=(0, 0)): + raise Exception('Not supported') + + class DxfLineStatement(DxfStatement): @classmethod def from_entity(cls, entity): @@ -93,9 +100,9 @@ class DxfLineStatement(DxfStatement): def to_metric(self): self.start = ( - metric(self.start[0]), inch(self.start[1])) + metric(self.start[0]), metric(self.start[1])) self.end = ( - metric(self.end[0]), inch(self.end[1])) + metric(self.end[0]), metric(self.end[1])) def is_equal_to(self, target, error_range=0): if not isinstance(target, DxfLineStatement): @@ -127,6 +134,14 @@ class DxfLineStatement(DxfStatement): y0 += yd d += pitch + def offset(self, offset_x, offset_y): + self.start = (self.start[0] + offset_x, self.start[1] + offset_y) + self.end = (self.end[0] + offset_x, self.end[1] + offset_y) + + def rotate(self, angle, center=(0, 0)): + self.start = rotate_point(self.start, angle, center) + self.end = rotate_point(self.end, angle, center) + class DxfCircleStatement(DxfStatement): def __init__(self, entity): super(DxfCircleStatement, self).__init__(entity) @@ -177,6 +192,12 @@ class DxfCircleStatement(DxfStatement): def reverse(self): pass + def offset(self, offset_x, offset_y): + self.center = (self.center[0] + offset_x, self.center[1] + offset_y) + + def rotate(self, angle, center=(0, 0)): + self.center = rotate_point(self.center, angle, center) + class DxfArcStatement(DxfStatement): def __init__(self, entity): super(DxfArcStatement, self).__init__(entity) @@ -251,6 +272,18 @@ class DxfArcStatement(DxfStatement): self.start = self.end self.end = tmp + def offset(self, offset_x, offset_y): + self.center = (self.center[0] + offset_x, self.center[1] + offset_y) + self.start = (self.start[0] + offset_x, self.start[1] + offset_y) + self.end = (self.end[0] + offset_x, self.end[1] + offset_y) + + def rotate(self, angle, center=(0, 0)): + self.start_angle += angle + self.end_angle += angle + self.center = rotate_point(self.center, angle, center) + self.start = rotate_point(self.start, angle, center) + self.end = rotate_point(self.end, angle, center) + class DxfPolylineStatement(DxfStatement): def __init__(self, entity): super(DxfPolylineStatement, self).__init__(entity) @@ -324,7 +357,6 @@ class DxfPolylineStatement(DxfStatement): 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): self.start = (metric(self.start[0]), metric(self.start[1])) @@ -332,7 +364,16 @@ class DxfPolylineStatement(DxfStatement): 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]) + + def offset(self, offset_x, offset_y): + for idx in range(len(self.entity.points)): + self.entity.points[idx] = ( + self.entity.points[idx][0] + offset_x, self.entity.points[idx][1] + offset_y) + + def rotate(self, angle, center=(0, 0)): + for idx in range(len(self.entity.points)): + self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center) + class DxfStatements(object): def __init__(self, statements, units, dcode=10, draw_mode=None): @@ -400,6 +441,14 @@ class DxfStatements(object): statement.to_metric() for path in self.paths: path.to_metric() + + def offset(self, offset_x, offset_y): + for statement in self.statements: + statement.offset(offset_x, offset_y) + + def rotate(self, angle, center=(0, 0)): + for statement in self.statements: + statement.rotate(angle, center) class DxfHeaderStatement(object): def to_gerber(self, settings): @@ -583,8 +632,11 @@ class DxfFile(CamFile): self.pitch = metric(self.pitch) self.units = 'metric' - def offset(self, ofset_x, offset_y): - raise Exception('Not supported') + def offset(self, offset_x, offset_y): + self.statements.offset(offset_x, offset_y) + + def rotate(self, angle, center=(0, 0)): + self.statements.rotate(angle, center) def loads(data, filename=None): if sys.version_info.major == 2: -- cgit From e3c59e39cf9bc64ce9d76c324b82956a65515f16 Mon Sep 17 00:00:00 2001 From: opiopan Date: Sun, 7 Apr 2019 22:22:33 +0900 Subject: expand test and fix many issues --- gerberex/am_expression.py | 4 +- gerberex/am_primitive.py | 16 +++--- gerberex/composition.py | 4 +- gerberex/excellon.py | 17 ++++++- gerberex/gerber_statements.py | 115 ++++++++++++++++++++++++++++++++++++++++++ gerberex/rs274x.py | 26 +++++++--- gerberex/statements.py | 70 ------------------------- gerberex/utility.py | 3 +- 8 files changed, 163 insertions(+), 92 deletions(-) create mode 100644 gerberex/gerber_statements.py delete mode 100644 gerberex/statements.py (limited to 'gerberex') diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py index 7aa95c8..b758df1 100644 --- a/gerberex/am_expression.py +++ b/gerberex/am_expression.py @@ -49,7 +49,7 @@ class AMConstantExpression(AMExpression): return self def to_gerber(self, settings=None): - return str(self._value) + return '%.6g' % self._value def to_instructions(self): return [(OpCode.PUSH, self._value)] @@ -179,5 +179,3 @@ def eval_macro(instructions): elif opcode == OpCode.PRIM: yield (argument, stack) stack = [] - - diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py index 82370f6..df55573 100644 --- a/gerberex/am_primitive.py +++ b/gerberex/am_primitive.py @@ -117,14 +117,14 @@ class AMVectorLinePrimitiveDef(AMPrimitiveDef): 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() + self.end_y = self.end_y.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() + self.end_y = self.end_y.to_metric().optimize() def to_gerber(self, settings=None): data = dict(code = self.code, @@ -197,11 +197,11 @@ class AMCenterLinePrimitiveDef(AMPrimitiveDef): class AMOutlinePrimitiveDef(AMPrimitiveDef): @classmethod def from_modifiers(cls, code, modifiers): - num_points = modifiers[1] + 1 + num_points = int(modifiers[1].value + 1) code = code exposure = 'on' if modifiers[0].value == 1 else 'off' - addrs = modifiers[2:num_points * 2] - rotation = modifiers[3 + num_points * 2] + addrs = modifiers[2:num_points * 2 + 2] + rotation = modifiers[2 + num_points * 2] return cls(code, exposure, addrs, rotation) def __init__(self, code, exposure, addrs, rotation): @@ -209,10 +209,10 @@ class AMOutlinePrimitiveDef(AMPrimitiveDef): self.addrs = addrs def to_inch(self): - self.addrs = [i.to_inch() for i in self.addrs] + self.addrs = [i.to_inch().optimize() for i in self.addrs] def to_metric(self): - self.addrs = [i.to_metric() for i in self.addrs] + self.addrs = [i.to_metric().optimize() for i in self.addrs] def to_gerber(self, settings=None): def strs(): @@ -262,7 +262,7 @@ class AMPolygonPrimitiveDef(AMPrimitiveDef): 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() + self.diameter = self.diameter.to_metric().optimize() def to_gerber(self, settings=None): data = dict(code = self.code, diff --git a/gerberex/composition.py b/gerberex/composition.py index 73f5702..7abf090 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -202,8 +202,8 @@ class DrillComposition(Composition): file.to_inch() if not self.header1_statements: - self.header1_statements = file.header - self.header2_statements = file.header2 + self.header1_statements = [file.header] + self.header2_statements = [file.header2] tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width)) self.dxf_statements.append((tool.number, file.statements)) diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 90d6742..b72b95b 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -6,6 +6,7 @@ from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) from gerber.excellon_statements import UnitStmt from gerber.cam import FileSettings +from gerber.utils import inch, metric from gerberex.utility import rotate def loads(data, filename=None, settings=None, tools=None, format=None): @@ -33,6 +34,19 @@ class ExcellonFileEx(ExcellonFile): return for hit in self.hits: hit.position = rotate(hit.position[0], hit.position[1], angle, center) + + def to_inch(self): + if self.units == 'metric': + super(ExcellonFileEx, self).to_inch() + for hit in self.hits: + hit.position = (inch(hit.position[0]), inch(hit.position[1])) + + def to_metric(self): + if self.units == 'inch': + super(ExcellonFileEx, self).to_metric() + for hit in self.hits: + hit.position = (metric(hit.position[0]), metric(hit.position[1])) + class UnitStmtEx(UnitStmt): @classmethod @@ -43,7 +57,8 @@ class UnitStmtEx(UnitStmt): super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs) def to_excellon(self, settings=None): + format = settings.format if settings else self.format 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]) + '0' * format[0], '0' * format[1]) return stmt diff --git a/gerberex/gerber_statements.py b/gerberex/gerber_statements.py new file mode 100644 index 0000000..c2eb565 --- /dev/null +++ b/gerberex/gerber_statements.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from gerber.gerber_statements import AMParamStmt, ADParamStmt +from gerber.utils import inch, metric +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, stmt.units) + + @classmethod + def circle(cls, name, units): + return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0', units) + + @classmethod + def rectangle(cls, name, units): + return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0', units) + + @classmethod + def landscape_obround(cls, name, units): + return cls( + 'AM', name, + '$4=$1-$2*' + '$5=$1-$4*' + '21,1,$5,$2,0,0,0*' + '1,1,$4,$4/2,0,0*' + '1,1,$4,-$4/2,0,0*' + '1,0,$3,0,0,0', units) + + @classmethod + def portrate_obround(cls, name, units): + return cls( + 'AM', name, + '$4=$2-$1*' + '$5=$2-$4*' + '21,1,$1,$5,0,0,0*' + '1,1,$4,0,$4/2,0*' + '1,1,$4,0,-$4/2,0*' + '1,0,$3,0,0,0', units) + + @classmethod + def polygon(cls, name, units): + return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0', units) + + def __init__(self, param, name, macro, units): + super(AMParamStmtEx, self).__init__(param, name, macro) + self.units = units + self.primitive_defs = list(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())) + + def rotate(self, angle, center=None): + for primitive_def in self.primitive_defs: + primitive_def.rotate(angle, center) + +class ADParamStmtEx(ADParamStmt): + GEOMETRIES = { + 'C': [0,1], + 'R': [0,1,2], + 'O': [0,1,2], + 'P': [0,3], + } + + @classmethod + def from_stmt(cls, stmt): + modstr = ','.join([ + 'X'.join(['{0}'.format(x) for x in modifier]) + for modifier in stmt.modifiers]) + return cls(stmt.param, stmt.d, stmt.shape, modstr, stmt.units) + + def __init__(self, param, d, shape, modifiers, units): + super(ADParamStmtEx, self).__init__(param, d, shape, modifiers) + self.units = units + + def to_inch(self): + if self.units == 'inch': + return + self.units = 'inch' + if self.shape in self.GEOMETRIES: + indices = self.GEOMETRIES[self.shape] + self.modifiers = [tuple([ + inch(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \ + for i in range(len(self.modifiers[0])) + ])] + + def to_metric(self): + if self.units == 'metric': + return + self.units = 'metric' + if self.shape in self.GEOMETRIES: + indices = self.GEOMETRIES[self.shape] + self.modifiers = [tuple([ + metric(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \ + for i in range(len(self.modifiers[0])) + ])] diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py index 4eb317d..13d3421 100644 --- a/gerberex/rs274x.py +++ b/gerberex/rs274x.py @@ -5,7 +5,7 @@ import gerber.rs274x from gerber.gerber_statements import ADParamStmt, CoordStmt -from gerberex.statements import AMParamStmt, AMParamStmtEx +from gerberex.gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx from gerberex.utility import rotate class GerberFile(gerber.rs274x.GerberFile): @@ -17,6 +17,8 @@ class GerberFile(gerber.rs274x.GerberFile): def swap_statement(statement): if isinstance(statement, AMParamStmt) and not isinstance(statement, AMParamStmtEx): return AMParamStmtEx.from_stmt(statement) + elif isinstance(statement, ADParamStmt) and not isinstance(statement, AMParamStmtEx): + return ADParamStmtEx.from_stmt(statement) else: return statement statements = [swap_statement(statement) for statement in gerber_file.statements] @@ -26,6 +28,18 @@ class GerberFile(gerber.rs274x.GerberFile): def __init__(self, statements, settings, primitives, apertures, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename) + def offset(self, x_offset=0, y_offset=0): + for statement in self.statements: + if isinstance(statement, CoordStmt): + if statement.x is not None: + statement.x += x_offset + if statement.y is not None: + statement.y += y_offset + else: + statement.offset(x_offset, y_offset) + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) + def rotate(self, angle, center=(0,0)): if angle % 360 == 0: return @@ -84,7 +98,7 @@ class GerberFile(gerber.rs274x.GerberFile): while name in macros: name = '%s_%d' % (macro_def[0], num) num += 1 - self.statements.insert(insert_point, macro_def[1](name)) + self.statements.insert(insert_point, macro_def[1](name, self.units)) macro_defs[idx] = (name, macro_def[1]) for idx in range(insert_point, last_aperture + len(macro_defs) + 1): statement = self.statements[idx] @@ -92,10 +106,10 @@ class GerberFile(gerber.rs274x.GerberFile): if statement.shape == 'R': statement.shape = macro_defs[RECTANGLE][0] elif statement.shape == 'O': - x = statement.modifiers[0] \ - if len(statement.modifiers) > 0 else 0 - y = statement.modifiers[1] \ - if len(statement.modifiers) > 1 else 0 + x = statement.modifiers[0][0] \ + if len(statement.modifiers[0]) > 0 else 0 + y = statement.modifiers[0][1] \ + if len(statement.modifiers[0]) > 1 else 0 statement.shape = macro_defs[LANDSCAPE_OBROUND][0] \ if x > y else macro_defs[PORTRATE_OBROUND][0] elif statement.shape == 'P': diff --git a/gerberex/statements.py b/gerberex/statements.py deleted file mode 100644 index c41acb9..0000000 --- a/gerberex/statements.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2019 Hiroshi Murayama - -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) - - @classmethod - def circle(cls, name): - return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0') - - @classmethod - def rectangle(cls, name): - return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0') - - @classmethod - def landscape_obround(cls, name): - return cls( - 'AM', name, - '$4=$1-$2*' - '21,1,$1-$4,$2,0,0,0*' - '1,1,$4,$4/2,0,0*' - '1,1,$4,-$4/2,0,0*' - '1,0,$3,0,0,0') - - @classmethod - def portrate_obround(cls, name): - return cls( - 'AM', name, - '$4=$2-$1*' - '21,1,$1,$2-$4,0,0,0*' - '1,1,$4,0,$4/2,0*' - '1,1,$4,0,-$4/2,0*' - '1,0,$3,0,0,0') - - @classmethod - def polygon(cls, name): - return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0') - - def __init__(self, param, name, macro): - super(AMParamStmtEx, self).__init__(param, name, macro) - self.primitive_defs = list(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())) - - def rotate(self, angle, center=None): - for primitive_def in self.primitive_defs: - primitive_def.rotate(angle, center) diff --git a/gerberex/utility.py b/gerberex/utility.py index f90df96..4c89fa6 100644 --- a/gerberex/utility.py +++ b/gerberex/utility.py @@ -13,8 +13,7 @@ def rotate(x, y, angle, center): sin(angle) * x0 + cos(angle) * y0 + center[1]) def is_equal_value(a, b, error_range=0): - return a - b <= error_range and a - b >= -error_range - + return (a - b) * (a - b) <= error_range * error_range def is_equal_point(a, b, error_range=0): return is_equal_value(a[0], b[0], error_range) and \ -- cgit From 6ec58842030b0bbe6aa3391456f017d71b74aeaa Mon Sep 17 00:00:00 2001 From: opiopan Date: Wed, 24 Apr 2019 22:55:23 +0900 Subject: fix a genarating closed path issue --- gerberex/dxf_path.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py index 2825c58..ca48d00 100644 --- a/gerberex/dxf_path.py +++ b/gerberex/dxf_path.py @@ -173,9 +173,20 @@ class DxfPath(object): def generate_closed_paths(statements, error_range=0): from gerberex.dxf import DxfLineStatement, DxfArcStatement - paths = [DxfPath(s, error_range) \ - for s in filter(lambda s: isinstance(s, DxfLineStatement) or \ - isinstance(s, DxfArcStatement), statements)] + unique_statements = [] + redundant = 0 + for statement in statements: + for target in unique_statements: + if not isinstance(statement, DxfLineStatement) and \ + not isinstance(statement, DxfArcStatement): + break + if statement.is_equal_to(target, error_range): + redundant += 1 + break + else: + unique_statements.append(statement) + + paths = [DxfPath(s, error_range) for s in unique_statements] prev_paths_num = 0 while prev_paths_num != len(paths): -- cgit From 89b5b714c9d2e00ebcf849783fc30bada956dd59 Mon Sep 17 00:00:00 2001 From: Marin Mikaël <41113988+MarinMikael@users.noreply.github.com> Date: Wed, 24 Jul 2019 01:19:09 +0900 Subject: Update composition.py Add DrillSlot support to excellon composition. --- gerberex/composition.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'gerberex') diff --git a/gerberex/composition.py b/gerberex/composition.py index 7abf090..29725ba 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -7,6 +7,7 @@ from functools import reduce from gerber.cam import FileSettings from gerber.gerber_statements import EofStmt from gerber.excellon_statements import * +from gerber.excellon import DrillSlot, DrillHit import gerberex.rs274x import gerberex.excellon import gerberex.dxf @@ -149,7 +150,10 @@ class DrillComposition(Composition): 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) + if type(h) == DrillSlot: + yield SlotStmt(*h.start, *h.end).to_excellon(self.settings) + elif type(h) == DrillHit: + yield CoordinateStmt(*h.position).to_excellon(self.settings) for num, statement in self.dxf_statements: if num == t.number: yield statement.to_excellon(self.settings) -- cgit From 415bdbc2e4339d370afcd779344ea66e39ba6b0a Mon Sep 17 00:00:00 2001 From: Marin Mikaël <41113988+MarinMikael@users.noreply.github.com> Date: Wed, 24 Jul 2019 01:20:15 +0900 Subject: Update am_primitive.py Fix bug when circle doesn't have any rotation by adding a default 0 degree rotation. --- gerberex/am_primitive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerberex') diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py index df55573..a3ad824 100644 --- a/gerberex/am_primitive.py +++ b/gerberex/am_primitive.py @@ -56,7 +56,7 @@ class AMCirclePrimitiveDef(AMPrimitiveDef): diameter = modifiers[1] center_x = modifiers[2] center_y = modifiers[3] - rotation = modifiers[4] + rotation = modifiers[4] if len(modifiers)>4 else AMConstantExpression(float(0)) return cls(code, exposure, diameter, center_x, center_y, rotation) def __init__(self, code, exposure, diameter, center_x, center_y, rotation): -- cgit From 0854d357587c96adb881255d0af6a7d40c15153c Mon Sep 17 00:00:00 2001 From: Marin Mikaël <41113988+MarinMikael@users.noreply.github.com> Date: Wed, 24 Jul 2019 01:24:41 +0900 Subject: Update am_expression.py String format to %f instead of %g. Scientific notation is supportd by Kicad and Ucamco's reference viewer, but causes artefacts on Altium and pcb-tools. --- gerberex/am_expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerberex') diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py index b758df1..4bb1d78 100644 --- a/gerberex/am_expression.py +++ b/gerberex/am_expression.py @@ -49,7 +49,7 @@ class AMConstantExpression(AMExpression): return self def to_gerber(self, settings=None): - return '%.6g' % self._value + return '%.6f' % self._value def to_instructions(self): return [(OpCode.PUSH, self._value)] -- cgit From 22f4c8a3f5bdce243908f3787216344b200902df Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sat, 17 Aug 2019 23:38:30 +0900 Subject: router mode and G85 slot in excellon file is supported --- gerberex/composition.py | 5 +- gerberex/excellon.py | 251 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 241 insertions(+), 15 deletions(-) (limited to 'gerberex') diff --git a/gerberex/composition.py b/gerberex/composition.py index 29725ba..7f691f5 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -150,10 +150,7 @@ class DrillComposition(Composition): yield ToolSelectionStmt(t.number).to_excellon(self.settings) for h in self.hits: if h.tool.number == t.number: - if type(h) == DrillSlot: - yield SlotStmt(*h.start, *h.end).to_excellon(self.settings) - elif type(h) == DrillHit: - yield CoordinateStmt(*h.position).to_excellon(self.settings) + yield h.to_excellon(self.settings) for num, statement in self.dxf_statements: if num == t.number: yield statement.to_excellon(self.settings) diff --git a/gerberex/excellon.py b/gerberex/excellon.py index b72b95b..657f02a 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -3,10 +3,15 @@ # Copyright 2019 Hiroshi Murayama -from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) -from gerber.excellon_statements import UnitStmt +import operator + +from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot +from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \ + ToolSelectionStmt, ZAxisRoutPositionStmt, \ + RetractWithClampingStmt, RetractWithoutClampingStmt, \ + EndOfProgramStmt from gerber.cam import FileSettings -from gerber.utils import inch, metric +from gerber.utils import inch, metric, write_gerber_value, parse_gerber_value from gerberex.utility import rotate def loads(data, filename=None, settings=None, tools=None, format=None): @@ -20,11 +25,93 @@ def loads(data, filename=None, settings=None, tools=None, format=None): 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 correct_statements(): + lazy_stmt = None + modifier = None + for stmt in file.statements: + modifier = lazy_stmt + lazy_stmt = None + if isinstance(stmt, UnitStmt): + yield UnitStmtEx.from_statement(stmt) + elif isinstance(stmt, CoordinateStmt): + new_stmt = CoordinateStmtEx.from_statement(stmt) + if modifier and new_stmt.mode is None: + new_stmt.mode = modifier + yield new_stmt + elif isinstance(stmt, UnknownStmt): + line = stmt.stmt.strip() + mode = None + if line[:3] == 'G02': + mode = CoordinateStmtEx.MODE_CIRCULER_CW + elif line[:3] == 'G03': + mode = CoordinateStmtEx.MODE_CIRCULER_CCW + else: + yield stmt + continue + if len(line) == 3: + lazy_stmt = mode + continue + new_stmt = CoordinateStmtEx.from_excellon(line[3:], file.settings) + new_stmt.mode = mode + yield new_stmt + else: + yield stmt + + def generate_hits(statements): + STAT_DRILL = 0 + STAT_ROUT_UP = 1 + STAT_ROUT_DOWN = 2 + status = STAT_DRILL + current_tool = None + rout_statements = [] + + def make_rout(status, statements): + if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None: + return None + return DrillRout.from_coordinates(current_tool, statements) + + for stmt in statements: + if isinstance(stmt, ToolSelectionStmt): + current_tool = file.tools[stmt.tool] + elif isinstance(stmt, DrillModeStmt): + rout = make_rout(status, rout_statements) + rout_statements = [] + if rout is not None: + yield rout + status = STAT_DRILL + elif isinstance(stmt, ZAxisRoutPositionStmt) and status == STAT_ROUT_UP: + status = STAT_ROUT_DOWN + elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt): + rout = make_rout(status, rout_statements) + rout_statements = [] + if rout is not None: + yield rout + status = STAT_ROUT_UP + elif isinstance(stmt, SlotStmt): + yield DrillSlotEx(current_tool, (stmt.x_start, stmt.y_start), + (stmt.x_end, stmt.y_end), DrillSlotEx.TYPE_G85) + elif isinstance(stmt, CoordinateStmtEx): + if stmt.mode is None: + if status != STAT_DRILL: + raise Exception('invalid statement sequence') + yield DrillHitEx(current_tool, (stmt.x, stmt.y)) + else: + if stmt.mode == stmt.MODE_ROUT: + status = STAT_ROUT_UP + if status == STAT_ROUT_UP: + rout_statements = [stmt] + elif status == STAT_ROUT_DOWN: + rout_statements.append(stmt) + else: + raise Exception('invalid statement sequence') + + statements = [s for s in correct_statements()] + hits = [h for h in generate_hits(statements)] + return cls(statements, file.tools, hits, file.settings, file.filename) + + @property + def primitives(self): + return [] def __init__(self, statements, tools, hits, settings, filename=None): super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename) @@ -33,20 +120,102 @@ class ExcellonFileEx(ExcellonFile): if angle % 360 == 0: return for hit in self.hits: - hit.position = rotate(hit.position[0], hit.position[1], angle, center) + hit.rotate(angle, center) def to_inch(self): if self.units == 'metric': super(ExcellonFileEx, self).to_inch() for hit in self.hits: - hit.position = (inch(hit.position[0]), inch(hit.position[1])) + hit.to_inch() def to_metric(self): if self.units == 'inch': super(ExcellonFileEx, self).to_metric() for hit in self.hits: - hit.position = (metric(hit.position[0]), metric(hit.position[1])) + hit.to_metric() + + def write(self, filename=None): + filename = filename if filename is not None else self.filename + with open(filename, 'w') as f: + + for statement in self.statements: + if not isinstance(statement, ToolSelectionStmt): + f.write(statement.to_excellon(self.settings) + '\n') + else: + break + + for tool in iter(self.tools.values()): + f.write(ToolSelectionStmt( + tool.number).to_excellon(self.settings) + '\n') + for hit in self.hits: + if hit.tool.number == tool.number: + f.write(hit.to_excellon(self.settings) + '\n') + f.write(EndOfProgramStmt().to_excellon() + '\n') + +class DrillHitEx(DrillHit): + def rotate(self, angle, center=(0,0)): + self.position = rotate(*self.position, angle, center) + + def to_excellon(self, settings): + return CoordinateStmtEx(*self.position).to_excellon(settings) + +class DrillSlotEx(DrillSlot): + def rotate(self, angle, center=(0,0)): + self.start = rotate(*self.start, angle, center) + self.end = rotate(*self.end, angle, center) + + def to_excellon(self, settings): + return SlotStmt(*self.start, *self.end).to_excellon(settings) + +class DrillRout(object): + class Node(object): + def __init__(self, mode, x, y, radius): + self.mode = mode + self.position = (x, y) + self.radius = radius + + @classmethod + def from_coordinates(cls, tool, coordinates): + nodes = [cls.Node(c.mode, c.x, c.y, c.radius) for c in coordinates] + return cls(tool, nodes) + + def __init__(self, tool, nodes): + self.tool = tool + self.nodes = nodes + def to_excellon(self, settings): + node = self.nodes[0] + excellon = CoordinateStmtEx(*node.position, node.radius, + CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n' + for node in self.nodes[1:]: + excellon += CoordinateStmtEx(*node.position, node.radius, + node.mode).to_excellon(settings) + '\n' + excellon += 'M16\nG05\n' + return excellon + + def to_inch(self): + if self.tool.settings.units == 'metric': + self.tool.to_inch() + for node in self.nodes: + node.position = tuple(map(inch, node.position)) + node.radius = inch( + node.radius) if node.radius is not None else None + + def to_metric(self): + if self.tool.settings.units == 'inch': + self.tool.to_metric() + for node in self.nodes: + node.position = tuple(map(metric, node.position)) + node.radius = metric( + node.radius) if node.radius is not None else None + + def offset(self, x_offset=0, y_offset=0): + for node in self.nodes: + node.position = tuple(map(operator.add, node.position, (x_offset, y_offset))) + + def rotate(self, angle, center=(0, 0)): + for node in self.nodes: + node.position = rotate(*node.position, angle, center) class UnitStmtEx(UnitStmt): @classmethod @@ -62,3 +231,63 @@ class UnitStmtEx(UnitStmt): 'LZ' if self.zeros == 'leading' else 'TZ', '0' * format[0], '0' * format[1]) return stmt + +class CoordinateStmtEx(CoordinateStmt): + MODE_ROUT = 'ROUT' + MODE_LINEAR = 'LINEAR' + MODE_CIRCULER_CW = 'CW' + MODE_CIRCULER_CCW = 'CCW' + + @classmethod + def from_statement(cls, stmt): + newStmt = cls(x=stmt.x, y=stmt.y) + newStmt.mode = stmt.mode + newStmt.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None + return newStmt + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + parts = line.split('A') + stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) + if len(parts) > 1: + stmt.radius = parse_gerber_value( + parts[1], settings.format, settings.zero_suppression) + return stmt + + def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs): + super(CoordinateStmtEx, self).__init__(x, y, **kwargs) + self.mode = mode + self.radius = radius + + def to_excellon(self, settings): + stmt = '' + if self.mode == self.MODE_ROUT: + stmt += "G00" + if self.mode == self.MODE_LINEAR: + stmt += "G01" + if self.mode == self.MODE_CIRCULER_CW: + stmt += "G02" + if self.mode == self.MODE_CIRCULER_CCW: + stmt += "G03" + if self.x is not None: + stmt += 'X%s' % write_gerber_value(self.x, settings.format, + settings.zero_suppression) + if self.y is not None: + stmt += 'Y%s' % write_gerber_value(self.y, settings.format, + settings.zero_suppression) + if self.radius is not None: + stmt += 'A%s' % write_gerber_value(self.radius, settings.format, + settings.zero_suppression) + return stmt + + def __str__(self): + coord_str = '' + if self.x is not None: + coord_str += 'X: %g ' % self.x + if self.y is not None: + coord_str += 'Y: %g ' % self.y + if self.radius is not None: + coord_str += 'A: %g ' % self.radius + + return '' % \ + (coord_str, self.mode if self.mode else 'HIT') -- cgit From 02258202793e9c70f212171fe4a92f6bab3a6f72 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sun, 18 Aug 2019 14:16:22 +0900 Subject: fix a bug that unit traslation of excellon fail --- gerberex/excellon.py | 48 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) (limited to 'gerberex') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 657f02a..04d10e9 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -124,13 +124,19 @@ class ExcellonFileEx(ExcellonFile): def to_inch(self): if self.units == 'metric': - super(ExcellonFileEx, self).to_inch() + for stmt in self.statements: + stmt.to_inch() + for tool in self.tools: + self.tools[tool].to_inch() for hit in self.hits: hit.to_inch() def to_metric(self): if self.units == 'inch': - super(ExcellonFileEx, self).to_metric() + for stmt in self.statements: + stmt.to_metric() + for tool in self.tools: + self.tools[tool].to_metric() for hit in self.hits: hit.to_metric() @@ -153,13 +159,27 @@ class ExcellonFileEx(ExcellonFile): f.write(EndOfProgramStmt().to_excellon() + '\n') class DrillHitEx(DrillHit): - def rotate(self, angle, center=(0,0)): + def to_inch(self): + self.position = tuple(map(inch, self.position)) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + + def rotate(self, angle, center=(0, 0)): self.position = rotate(*self.position, angle, center) def to_excellon(self, settings): return CoordinateStmtEx(*self.position).to_excellon(settings) class DrillSlotEx(DrillSlot): + def to_inch(self): + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + + def to_metric(self): + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + def rotate(self, angle, center=(0,0)): self.start = rotate(*self.start, angle, center) self.end = rotate(*self.end, angle, center) @@ -190,24 +210,20 @@ class DrillRout(object): for node in self.nodes[1:]: excellon += CoordinateStmtEx(*node.position, node.radius, node.mode).to_excellon(settings) + '\n' - excellon += 'M16\nG05\n' + excellon += 'M16\nG05' return excellon def to_inch(self): - if self.tool.settings.units == 'metric': - self.tool.to_inch() - for node in self.nodes: - node.position = tuple(map(inch, node.position)) - node.radius = inch( - node.radius) if node.radius is not None else None + for node in self.nodes: + node.position = tuple(map(inch, node.position)) + node.radius = inch( + node.radius) if node.radius is not None else None def to_metric(self): - if self.tool.settings.units == 'inch': - self.tool.to_metric() - for node in self.nodes: - node.position = tuple(map(metric, node.position)) - node.radius = metric( - node.radius) if node.radius is not None else None + for node in self.nodes: + node.position = tuple(map(metric, node.position)) + node.radius = metric( + node.radius) if node.radius is not None else None def offset(self, x_offset=0, y_offset=0): for node in self.nodes: -- cgit From f8fe16708502e0588d9d0be42d097a933d6f12d3 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sun, 18 Aug 2019 18:33:41 +0900 Subject: zero supressing in aperture macro definition --- gerberex/am_expression.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'gerberex') diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py index 4bb1d78..5400130 100644 --- a/gerberex/am_expression.py +++ b/gerberex/am_expression.py @@ -49,7 +49,8 @@ class AMConstantExpression(AMExpression): return self def to_gerber(self, settings=None): - return '%.6f' % self._value + gerber = '%.6g' % self._value + return '%.6f' % self._value if 'e' in gerber else gerber def to_instructions(self): return [(OpCode.PUSH, self._value)] -- cgit From 36956f93fe1773229b6b7bb1b851adf169f97f79 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sun, 25 Aug 2019 12:39:01 +0900 Subject: improve routing mode compatibility with excellon specification --- gerberex/excellon.py | 242 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 166 insertions(+), 76 deletions(-) (limited to 'gerberex') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 04d10e9..7014a6e 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -5,8 +5,10 @@ import operator +import gerber.excellon from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot -from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \ +from gerber.excellon_statements import ExcellonStatement, UnitStmt, CoordinateStmt, UnknownStmt, \ + SlotStmt, DrillModeStmt, RouteModeStmt, LinearModeStmt, \ ToolSelectionStmt, ZAxisRoutPositionStmt, \ RetractWithClampingStmt, RetractWithoutClampingStmt, \ EndOfProgramStmt @@ -19,6 +21,8 @@ def loads(data, filename=None, settings=None, tools=None, format=None): settings = FileSettings(**detect_excellon_format(data)) if format: settings.format = format + gerber.excellon.CoordinateStmt = CoordinateStmtEx + gerber.excellon.UnitStmt = UnitStmtEx file = ExcellonParser(settings, tools).parse_raw(data, filename) return ExcellonFileEx.from_file(file) @@ -26,49 +30,78 @@ class ExcellonFileEx(ExcellonFile): @classmethod def from_file(cls, file): def correct_statements(): - lazy_stmt = None - modifier = None for stmt in file.statements: - modifier = lazy_stmt - lazy_stmt = None - if isinstance(stmt, UnitStmt): - yield UnitStmtEx.from_statement(stmt) - elif isinstance(stmt, CoordinateStmt): - new_stmt = CoordinateStmtEx.from_statement(stmt) - if modifier and new_stmt.mode is None: - new_stmt.mode = modifier - yield new_stmt - elif isinstance(stmt, UnknownStmt): + if isinstance(stmt, UnknownStmt): line = stmt.stmt.strip() - mode = None if line[:3] == 'G02': - mode = CoordinateStmtEx.MODE_CIRCULER_CW + yield CircularCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) elif line[:3] == 'G03': - mode = CoordinateStmtEx.MODE_CIRCULER_CCW + yield CircularCCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) + elif line[0] == 'X' or line[0] == 'Y' or line[0] == 'A' or line[0] == 'I': + yield CoordinateStmtEx.from_excellon(line, file.settings) else: yield stmt - continue - if len(line) == 3: - lazy_stmt = mode - continue - new_stmt = CoordinateStmtEx.from_excellon(line[3:], file.settings) - new_stmt.mode = mode - yield new_stmt else: yield stmt def generate_hits(statements): + class CoordinateCtx: + def __init__(self, notation): + self.notation = notation + self.x = 0. + self.y = 0. + self.radius = None + self.center_offset = None + + def update(self, x=None, y=None, radius=None, center_offset=None): + if self.notation == 'absolute': + if x is not None: + self.x = x + if y is not None: + self.y = y + else: + if x is not None: + self.x += x + if y is not None: + self.y += y + if radius is not None: + self.radius = radius + if center_offset is not None: + self.center_offset = center_offset + + def node(self, mode, center_offset): + radius, offset = None, None + if mode == DrillRout.MODE_CIRCULER_CW or mode == DrillRout.MODE_CIRCULER_CCW: + if center_offset is None: + radius = self.radius + offset = self.center_offset + else: + radius = None + offset = center_offset + return DrillRout.Node(mode, self.x, self.y, radius, offset) + STAT_DRILL = 0 STAT_ROUT_UP = 1 STAT_ROUT_DOWN = 2 + status = STAT_DRILL current_tool = None - rout_statements = [] + rout_mode = None + coordinate_ctx = CoordinateCtx(file.notation) + rout_nodes = [] + + last_position = (0., 0.) + last_radius = None + last_center_offset = None - def make_rout(status, statements): - if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None: + def make_rout(status, nodes): + if status != STAT_ROUT_DOWN or len(nodes) == 0 or current_tool is None: return None - return DrillRout.from_coordinates(current_tool, statements) + return DrillRout(current_tool, nodes) for stmt in statements: if isinstance(stmt, ToolSelectionStmt): @@ -79,31 +112,48 @@ class ExcellonFileEx(ExcellonFile): if rout is not None: yield rout status = STAT_DRILL + rout_mode = None + elif isinstance(stmt, RouteModeStmt): + if status == STAT_DRILL: + status = STAT_ROUT_UP + rout_mode = DrillRout.MODE_ROUT + else: + rout_mode = DrillRout.MODE_LINEAR + + elif isinstance(stmt, LinearModeStmt): + rout_mode = DrillRout.MODE_LINEAR + elif isinstance(stmt, CircularCWModeStmt): + rout_mode = DrillRout.MODE_CIRCULER_CW + elif isinstance(stmt, CircularCCWModeStmt): + rout_mode = DrillRout.MODE_CIRCULER_CCW elif isinstance(stmt, ZAxisRoutPositionStmt) and status == STAT_ROUT_UP: status = STAT_ROUT_DOWN elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt): - rout = make_rout(status, rout_statements) + rout = make_rout(status, rout_nodes) rout_statements = [] if rout is not None: yield rout status = STAT_ROUT_UP elif isinstance(stmt, SlotStmt): - yield DrillSlotEx(current_tool, (stmt.x_start, stmt.y_start), - (stmt.x_end, stmt.y_end), DrillSlotEx.TYPE_G85) + coordinate_ctx.update(stmt.x_start, stmt.y_start) + x_start = coordinate_ctx.x + y_start = coordinate_ctx.y + coordinate_ctx.update(stmt.x_end, stmt.y_end) + x_end = coordinate_ctx.x + y_end = coordinate_ctx.y + yield DrillSlotEx(current_tool, (x_start, y_start), + (x_end, y_end), DrillSlotEx.TYPE_G85) elif isinstance(stmt, CoordinateStmtEx): - if stmt.mode is None: - if status != STAT_DRILL: - raise Exception('invalid statement sequence') - yield DrillHitEx(current_tool, (stmt.x, stmt.y)) - else: - if stmt.mode == stmt.MODE_ROUT: - status = STAT_ROUT_UP - if status == STAT_ROUT_UP: - rout_statements = [stmt] + center_offset = (stmt.i, stmt.j) \ + if stmt.i is not None and stmt.j is not None else None + coordinate_ctx.update(stmt.x, stmt.y, stmt.radius, center_offset) + if stmt.x is not None or stmt.y is not None: + if status == STAT_DRILL: + yield DrillHitEx(current_tool, (coordinate_ctx.x, coordinate_ctx.y)) + elif status == STAT_ROUT_UP: + rout_nodes = [coordinate_ctx.node(DrillRout.MODE_ROUT, None)] elif status == STAT_ROUT_DOWN: - rout_statements.append(stmt) - else: - raise Exception('invalid statement sequence') + rout_nodes.append(coordinate_ctx.node(rout_mode, center_offset)) statements = [s for s in correct_statements()] hits = [h for h in generate_hits(statements)] @@ -188,28 +238,33 @@ class DrillSlotEx(DrillSlot): return SlotStmt(*self.start, *self.end).to_excellon(settings) class DrillRout(object): + MODE_ROUT = 'G00' + MODE_LINEAR = 'G01' + MODE_CIRCULER_CW = 'G02' + MODE_CIRCULER_CCW = 'G03' + class Node(object): - def __init__(self, mode, x, y, radius): + def __init__(self, mode, x, y, radius=None, center_offset=None): self.mode = mode self.position = (x, y) self.radius = radius + self.center_offset = center_offset - @classmethod - def from_coordinates(cls, tool, coordinates): - nodes = [cls.Node(c.mode, c.x, c.y, c.radius) for c in coordinates] - return cls(tool, nodes) + def to_excellon(self, settings): + center_offset = self.center_offset \ + if self.center_offset is not None else (None, None) + return self.mode + CoordinateStmtEx( + *self.position, self.radius, *center_offset).to_excellon(settings) def __init__(self, tool, nodes): self.tool = tool self.nodes = nodes + self.nodes[0].mode = self.MODE_ROUT def to_excellon(self, settings): - node = self.nodes[0] - excellon = CoordinateStmtEx(*node.position, node.radius, - CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n' + excellon = self.nodes[0].to_excellon(settings) + '\nM15\n' for node in self.nodes[1:]: - excellon += CoordinateStmtEx(*node.position, node.radius, - node.mode).to_excellon(settings) + '\n' + excellon += node.to_excellon(settings) + '\n' excellon += 'M16\nG05' return excellon @@ -218,12 +273,16 @@ class DrillRout(object): node.position = tuple(map(inch, node.position)) node.radius = inch( node.radius) if node.radius is not None else None + if node.center_offset is not None: + node.center_offset = tuple(map(inch, node.center_offset)) def to_metric(self): for node in self.nodes: node.position = tuple(map(metric, node.position)) node.radius = metric( node.radius) if node.radius is not None else None + if node.center_offset is not None: + node.center_offset = tuple(map(metric, node.center_offset)) def offset(self, x_offset=0, y_offset=0): for node in self.nodes: @@ -232,6 +291,8 @@ class DrillRout(object): def rotate(self, angle, center=(0, 0)): for node in self.nodes: node.position = rotate(*node.position, angle, center) + if node.center_offset is not None: + node.center_offset = rotate(*node.center_offset, angle, (0., 0.)) class UnitStmtEx(UnitStmt): @classmethod @@ -243,48 +304,69 @@ class UnitStmtEx(UnitStmt): def to_excellon(self, settings=None): format = settings.format if settings else self.format - stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC', - 'LZ' if self.zeros == 'leading' else 'TZ', - '0' * format[0], '0' * format[1]) + stmt = None + if self.units == 'inch' and format == (2, 4): + stmt = 'INCH,%s' % ('LZ' if self.zeros == 'leading' else 'TZ') + else: + stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC', + 'LZ' if self.zeros == 'leading' else 'TZ', + '0' * format[0], '0' * format[1]) return stmt -class CoordinateStmtEx(CoordinateStmt): - MODE_ROUT = 'ROUT' - MODE_LINEAR = 'LINEAR' - MODE_CIRCULER_CW = 'CW' - MODE_CIRCULER_CCW = 'CCW' +class CircularCWModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CircularCWModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G02' +class CircularCCWModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CircularCCWModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G02' + +class CoordinateStmtEx(CoordinateStmt): @classmethod def from_statement(cls, stmt): newStmt = cls(x=stmt.x, y=stmt.y) - newStmt.mode = stmt.mode newStmt.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None return newStmt @classmethod def from_excellon(cls, line, settings, **kwargs): - parts = line.split('A') - stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) - if len(parts) > 1: + stmt = None + if 'A' in line: + parts = line.split('A') + stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) \ + if parts[0] != '' else cls() stmt.radius = parse_gerber_value( parts[1], settings.format, settings.zero_suppression) + elif 'I' in line: + jparts = line.split('J') + iparts = jparts[0].split('I') + stmt = cls.from_statement(CoordinateStmt.from_excellon(iparts[0], settings)) \ + if iparts[0] != '' else cls() + stmt.i = parse_gerber_value( + iparts[1], settings.format, settings.zero_suppression) + stmt.j = parse_gerber_value( + jparts[1], settings.format, settings.zero_suppression) + else: + stmt = cls.from_statement(CoordinateStmt.from_excellon(line, settings)) + return stmt - def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs): + def __init__(self, x=None, y=None, radius=None, i=None, j=None, **kwargs): super(CoordinateStmtEx, self).__init__(x, y, **kwargs) - self.mode = mode self.radius = radius + self.i = i + self.j = j def to_excellon(self, settings): stmt = '' - if self.mode == self.MODE_ROUT: - stmt += "G00" - if self.mode == self.MODE_LINEAR: - stmt += "G01" - if self.mode == self.MODE_CIRCULER_CW: - stmt += "G02" - if self.mode == self.MODE_CIRCULER_CCW: - stmt += "G03" if self.x is not None: stmt += 'X%s' % write_gerber_value(self.x, settings.format, settings.zero_suppression) @@ -294,6 +376,11 @@ class CoordinateStmtEx(CoordinateStmt): if self.radius is not None: stmt += 'A%s' % write_gerber_value(self.radius, settings.format, settings.zero_suppression) + elif self.i is not None and self.j is not None: + stmt += 'I%sJ%s' % (write_gerber_value(self.i, settings.format, + settings.zero_suppression), + write_gerber_value(self.j, settings.format, + settings.zero_suppression)) return stmt def __str__(self): @@ -304,6 +391,9 @@ class CoordinateStmtEx(CoordinateStmt): coord_str += 'Y: %g ' % self.y if self.radius is not None: coord_str += 'A: %g ' % self.radius + if self.i is not None: + coord_str += 'I: %g ' % self.i + if self.j is not None: + coord_str += 'J: %g ' % self.j - return '' % \ - (coord_str, self.mode if self.mode else 'HIT') + return '' % (coord_str) -- cgit From 13ab9db6e7571f3d0fcb406bfe6795eea9ce4e1c Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sun, 25 Aug 2019 20:16:53 +0900 Subject: support incremental coordinate for excellon --- gerberex/composition.py | 27 +-------------------------- gerberex/dxf.py | 31 +++---------------------------- gerberex/excellon.py | 17 ++++++++++------- 3 files changed, 14 insertions(+), 61 deletions(-) (limited to 'gerberex') diff --git a/gerberex/composition.py b/gerberex/composition.py index 7f691f5..7b1548e 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -124,8 +124,6 @@ class GerberComposition(Composition): 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 = [] self.dxf_statements = [] @@ -140,12 +138,6 @@ class DrillComposition(Composition): 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: @@ -157,6 +149,7 @@ class DrillComposition(Composition): yield EndOfProgramStmt().to_excellon() with open(path, 'w') as f: + gerberex.excellon.write_excellon_header(f, self.settings, self.tools) for statement in statements(): f.write(statement + '\n') @@ -171,20 +164,6 @@ class DrillComposition(Composition): 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) @@ -202,10 +181,6 @@ class DrillComposition(Composition): else: file.to_inch() - if not self.header1_statements: - self.header1_statements = [file.header] - self.header2_statements = [file.header2] - tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width)) self.dxf_statements.append((tool.number, file.statements)) diff --git a/gerberex/dxf.py b/gerberex/dxf.py index ba02f08..389cca9 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -13,6 +13,7 @@ from gerber.excellon_statements import ExcellonTool from gerber.excellon_statements import CoordinateStmt from gerberex.utility import is_equal_point, is_equal_value from gerberex.dxf_path import generate_closed_paths +from gerberex.excellon import write_excellon_header ACCEPTABLE_ERROR = 0.001 @@ -465,31 +466,8 @@ class DxfHeaderStatement(object): ) def to_excellon(self, settings): - return 'M48\n'\ - 'FMAT,2\n'\ - 'ICI,{0}\n'\ - '{1},{2},{3}.{4}\n'\ - '{5}'.format( - 'ON' if settings.notation == 'incremental' else 'OFF', - 'INCH' if settings.units == 'inch' else 'METRIC', - 'TZ' if settings.zero_suppression == 'leading' else 'LZ', - '0' * settings.format[0], '0' * settings.format[1], - 'M72' if settings.units == 'inch' else 'M71' - ) - - def to_inch(self): - pass - - def to_metric(self): pass -class DxfHeader2Statement(object): - def to_gerber(self, settings): - pass - - def to_excellon(self, settings): - return '%' - def to_inch(self): pass @@ -555,7 +533,6 @@ class DxfFile(CamFile): self._draw_mode = draw_mode self.header = DxfHeaderStatement() - self.header2 = DxfHeader2Statement() self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0) self.statements = DxfStatements( statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode) @@ -607,10 +584,8 @@ class DxfFile(CamFile): f.write(self.statements.to_gerber(self.settings) + '\n') f.write('M02*\n') else: - tool = ExcellonTool(self.settings, number=1, diameter=self.width) - f.write(self.header.to_excellon(self.settings) + '\n') - f.write(tool.to_excellon(self.settings) + '\n') - f.write(self.header2.to_excellon(self.settings) + '\n') + tools = [ExcellonTool(self.settings, number=1, diameter=self.width)] + write_excellon_header(f, self.settings, tools) f.write('T01\n') f.write(self.statements.to_excellon(self.settings) + '\n') f.write('M30\n') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 7014a6e..a8c01c7 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -26,6 +26,13 @@ def loads(data, filename=None, settings=None, tools=None, format=None): file = ExcellonParser(settings, tools).parse_raw(data, filename) return ExcellonFileEx.from_file(file) +def write_excellon_header(file, settings, tools): + file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % + UnitStmtEx(settings.units, settings.zeros, settings.format).to_excellon(settings)) + for tool in tools: + file.write(tool.to_excellon(settings) + '\n') + file.write('%%\nG90\n%s\n' % ('M72' if settings.units == 'inch' else 'M71')) + class ExcellonFileEx(ExcellonFile): @classmethod def from_file(cls, file): @@ -180,6 +187,7 @@ class ExcellonFileEx(ExcellonFile): self.tools[tool].to_inch() for hit in self.hits: hit.to_inch() + self.units = 'inch' def to_metric(self): if self.units == 'inch': @@ -189,17 +197,12 @@ class ExcellonFileEx(ExcellonFile): self.tools[tool].to_metric() for hit in self.hits: hit.to_metric() + self.units = 'metric' def write(self, filename=None): filename = filename if filename is not None else self.filename with open(filename, 'w') as f: - - for statement in self.statements: - if not isinstance(statement, ToolSelectionStmt): - f.write(statement.to_excellon(self.settings) + '\n') - else: - break - + write_excellon_header(f, self.settings, [self.tools[t] for t in self.tools]) for tool in iter(self.tools.values()): f.write(ToolSelectionStmt( tool.number).to_excellon(self.settings) + '\n') -- cgit From 2b1c751ff76ebd6901633235ee694cc93dabce81 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Mon, 9 Sep 2019 09:07:38 +0900 Subject: improve compatibility with RS-274x specification: - can merge multiple files having different file scope modifier, such as AS, MI, OF, SF, and IR - support modal coordinate notation --- gerberex/common.py | 2 +- gerberex/composition.py | 53 +++++------ gerberex/dxf.py | 31 +------ gerberex/excellon.py | 2 +- gerberex/rs274x.py | 241 +++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 238 insertions(+), 91 deletions(-) (limited to 'gerberex') diff --git a/gerberex/common.py b/gerberex/common.py index 47bc6b3..6e8a832 100644 --- a/gerberex/common.py +++ b/gerberex/common.py @@ -25,7 +25,7 @@ def loads(data, filename=None, format=None): fmt = detect_file_format(data) if fmt == 'rs274x': - file = gerber.rs274x.loads(data, filename=filename) + file = gerberex.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) diff --git a/gerberex/composition.py b/gerberex/composition.py index 7b1548e..634640e 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -22,7 +22,6 @@ class GerberComposition(Composition): def __init__(self, settings=None, comments=None): super(GerberComposition, self).__init__(settings, comments) - self.param_statements = [] self.aperture_macros = {} self.apertures = [] self.drawings = [] @@ -37,8 +36,6 @@ class GerberComposition(Composition): 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: @@ -46,12 +43,14 @@ class GerberComposition(Composition): for s in self.drawings: yield s yield EofStmt() + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' with open(path, 'w') as f: + gerberex.rs274x.write_gerber_header(f, self.settings) for statement in statements(): f.write(statement.to_gerber(self.settings) + '\n') def _merge_gerber(self, file): - param_statements = [] aperture_macro_map = {} aperture_map = {} @@ -61,34 +60,27 @@ class GerberComposition(Composition): 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) + for macro in file.aperture_macros: + statement = file.aperture_macros[macro] + name = statement.name + newname = self._register_aperture_macro(statement) + aperture_macro_map[name] = newname + + for statement in file.aperture_defs: + if statement.param == 'AD': + if statement.shape in aperture_macro_map: + statement.shape = aperture_macro_map[statement.shape] + dnum = statement.d + newdnum = self._register_aperture(statement) + aperture_map[dnum] = newdnum + + for statement in file.main_statements: + 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 + self.settings = file.context def _merge_dxf(self, file): if self.settings: @@ -102,7 +94,6 @@ class GerberComposition(Composition): if not self.settings: self.settings = file.settings - self.param_statements = [file.header] def _register_aperture_macro(self, statement): diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 389cca9..ae543ae 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -14,6 +14,7 @@ from gerber.excellon_statements import CoordinateStmt from gerberex.utility import is_equal_point, is_equal_value from gerberex.dxf_path import generate_closed_paths from gerberex.excellon import write_excellon_header +from gerberex.rs274x import write_gerber_header ACCEPTABLE_ERROR = 0.001 @@ -395,6 +396,8 @@ class DxfStatements(object): def to_gerber(self, settings=FileSettings()): def gerbers(): + yield 'G75*' + yield '%LPD*%' yield 'D{0}*'.format(self.dcode) if self.draw_mode == DxfFile.DM_FILL: yield 'G36*' @@ -451,29 +454,6 @@ class DxfStatements(object): for statement in self.statements: statement.rotate(angle, center) -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_excellon(self, settings): - pass - - def to_inch(self): - pass - - def to_metric(self): - pass - class DxfFile(CamFile): DM_LINE = 0 DM_FILL = 1 @@ -531,7 +511,6 @@ class DxfFile(CamFile): 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( @@ -579,7 +558,7 @@ class DxfFile(CamFile): filename = filename if filename is not None else self.filename with open(filename, 'w') as f: if filetype == self.FT_RX274X: - f.write(self.header.to_gerber(self.settings) + '\n') + write_gerber_header(f, self.settings) f.write(self.aperture.to_gerber(self.settings) + '\n') f.write(self.statements.to_gerber(self.settings) + '\n') f.write('M02*\n') @@ -593,7 +572,6 @@ class DxfFile(CamFile): def to_inch(self): if self.units == 'metric': - self.header.to_inch() self.aperture.to_inch() self.statements.to_inch() self.pitch = inch(self.pitch) @@ -601,7 +579,6 @@ class DxfFile(CamFile): def to_metric(self): if self.units == 'inch': - self.header.to_metric() self.aperture.to_metric() self.statements.to_metric() self.pitch = metric(self.pitch) diff --git a/gerberex/excellon.py b/gerberex/excellon.py index a8c01c7..4f867be 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -27,7 +27,7 @@ def loads(data, filename=None, settings=None, tools=None, format=None): return ExcellonFileEx.from_file(file) def write_excellon_header(file, settings, tools): - file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % + file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % UnitStmtEx(settings.units, settings.zeros, settings.format).to_excellon(settings)) for tool in tools: file.write(tool.to_excellon(settings) + '\n') diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py index 13d3421..fae0b32 100644 --- a/gerberex/rs274x.py +++ b/gerberex/rs274x.py @@ -3,10 +3,29 @@ # Copyright 2019 Hiroshi Murayama +from gerber.cam import FileSettings import gerber.rs274x -from gerber.gerber_statements import ADParamStmt, CoordStmt +from gerber.gerber_statements import * from gerberex.gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx from gerberex.utility import rotate +import re + +def loads(data, filename=None): + cls = gerber.rs274x.GerberParser + cls.SF = \ + r"(?PSF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=cls.DECIMAL) + cls.PARAMS = (cls.FS, cls.MO, cls.LP, cls.AD_CIRCLE, + cls.AD_RECT, cls.AD_OBROUND, cls.AD_POLY, + cls.AD_MACRO, cls.AM, cls.AS, cls.IF, cls.IN, + cls.IP, cls.IR, cls.MI, cls.OF, cls.SF, cls.LN) + cls.PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in cls.PARAMS] + return cls().parse_raw(data, filename) + +def write_gerber_header(file, settings): + file.write('%s\n%s\n%%IPPOS*%%\n' % ( + MOParamStmt('MO', settings.units).to_gerber(settings), + FSParamStmt('FS', settings.zero_suppression, + settings.notation, settings.format).to_gerber(settings))) class GerberFile(gerber.rs274x.GerberFile): @classmethod @@ -14,29 +33,74 @@ class GerberFile(gerber.rs274x.GerberFile): 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) - elif isinstance(statement, ADParamStmt) and not isinstance(statement, AMParamStmtEx): - return ADParamStmtEx.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,\ + return cls(gerber_file.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) + self.context = GerberContext.from_settings(self.settings) + self.aperture_macros = {} + self.aperture_defs = [] + self.main_statements = [] + for stmt in self.statements: + type, stmts = self.context.normalize_statement(stmt) + if type == self.context.TYPE_AM: + for mdef in stmts: + self.aperture_macros[mdef.name] = mdef + elif type == self.context.TYPE_AD: + self.aperture_defs.extend(stmts) + elif type == self.context.TYPE_MAIN: + self.main_statements.extend(stmts) + if self.context.angle != 0: + self.rotate(self.context.angle) + self.context.notation = 'absolute' + self.context.zeros = 'trailing' + + def write(self, filename=None): + self.context.notation = 'absolute' + self.context.zeros = 'trailing' + self.context.format = self.format + self.units = self.units + filename=filename if filename is not None else self.filename + with open(filename, 'w') as f: + write_gerber_header(f, self.context) + for macro in self.aperture_macros: + f.write(self.aperture_macros[macro].to_gerber(self.context) + '\n') + for aperture in self.aperture_defs: + f.write(aperture.to_gerber(self.context) + '\n') + for statement in self.main_statements: + f.write(statement.to_gerber(self.context) + '\n') + f.write('M02*\n') + + def to_inch(self): + if self.units == 'metric': + for macro in self.aperture_macros: + self.aperture_macros[macro].to_inch() + for aperture in self.aperture_defs: + aperture.to_inch() + for statement in self.statements: + statement.to_inch() + self.units = 'inch' + self.context.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + for macro in self.aperture_macros: + self.aperture_macros[macro].to_metric() + for aperture in self.aperture_defs: + aperture.to_metric() + for statement in self.statements: + statement.to_metric() + self.units='metric' + self.context.units='metric' def offset(self, x_offset=0, y_offset=0): - for statement in self.statements: + for statement in self.main_statements: if isinstance(statement, CoordStmt): if statement.x is not None: statement.x += x_offset if statement.y is not None: statement.y += y_offset - else: - statement.offset(x_offset, y_offset) for primitive in self.primitives: primitive.offset(x_offset, y_offset) @@ -48,10 +112,10 @@ class GerberFile(gerber.rs274x.GerberFile): last_y = 0 last_rx = 0 last_ry = 0 - for statement in self.statements: - if isinstance(statement, AMParamStmtEx): - statement.rotate(angle, center) - elif isinstance(statement, CoordStmt) and statement.x != None and statement.y != None: + for name in self.aperture_macros: + self.aperture_macros[name].rotate(angle, center) + for statement in self.main_statements: + if isinstance(statement, CoordStmt) and statement.x != None and statement.y != None: if statement.i != None and statement.j != None: cx = last_x + statement.i cy = last_y + statement.j @@ -77,31 +141,21 @@ class GerberFile(gerber.rs274x.GerberFile): ] need_to_change = False - insert_point = 0 - last_aperture = 0 - macros = {} - for idx in range(0, len(self.statements)): - statement = self.statements[idx] - if isinstance(statement, AMParamStmtEx): - macros[statement.name] = statement - if not need_to_change: - insert_point = idx + 1 + for statement in self.aperture_defs: if isinstance(statement, ADParamStmt) and statement.shape in ['R', 'O', 'P']: need_to_change = True - last_aperture = idx if need_to_change: for idx in range(0, len(macro_defs)): macro_def = macro_defs[idx] name = macro_def[0] num = 1 - while name in macros: + while name in self.aperture_macros: name = '%s_%d' % (macro_def[0], num) num += 1 - self.statements.insert(insert_point, macro_def[1](name, self.units)) + self.aperture_macros[name] = macro_def[1](name, self.units) macro_defs[idx] = (name, macro_def[1]) - for idx in range(insert_point, last_aperture + len(macro_defs) + 1): - statement = self.statements[idx] + for statement in self.aperture_defs: if isinstance(statement, ADParamStmt): if statement.shape == 'R': statement.shape = macro_defs[RECTANGLE][0] @@ -114,3 +168,128 @@ class GerberFile(gerber.rs274x.GerberFile): if x > y else macro_defs[PORTRATE_OBROUND][0] elif statement.shape == 'P': statement.shape = macro_defs[POLYGON][0] + +class GerberContext(FileSettings): + TYPE_NONE = 'none' + TYPE_AM = 'am' + TYPE_AD = 'ad' + TYPE_MAIN = 'main' + IP_LINEAR = 'lenear' + IP_ARC = 'arc' + DIR_CLOCKWISE = 'cw' + DIR_COUNTERCLOCKWISE = 'ccw' + + ignored_stmt = ('FSParamStmt', 'MOParamStmt', 'ASParamStmt', + 'INParamStmt', 'IPParamStmt', 'IRParamStmt', + 'MIParamStmt', 'OFParamStmt', 'SFParamStmt', + 'LNParamStmt', 'CommentStmt', 'EofStmt',) + + @classmethod + def from_settings(cls, settings): + return cls(settings.notation, settings.units, settings.zero_suppression, + settings.format, settings.zeros, settings.angle_units) + + def __init__(self, notation='absolute', units='inch', + zero_suppression=None, format=(2, 5), zeros=None, + angle_units='degrees', + name=None, + mirror=(False, False), offset=(0., 0.), scale=(1., 1.), + angle=0., axis='xy'): + super(GerberContext, self).__init__(notation, units, zero_suppression, + format, zeros, angle_units) + self.name = name + self.mirror = mirror + self.offset = offset + self.scale = scale + self.angle = angle + self.axis = axis + + self.matrix = (1, 0, + 1, 0, + 1, 1) + + self.op = None + self.interpolation = self.IP_LINEAR + self.direction = self.DIR_CLOCKWISE + self.x = 0. + self.y = 0. + + def normalize_statement(self, stmt): + if isinstance(stmt, INParamStmt): + self.name = stmt.name + elif isinstance(stmt, MIParamStmt): + self.mirror = (stmt.a, stmt.b) + self._update_matrix() + elif isinstance(stmt, OFParamStmt): + self.offset = (stmt.a, stmt.b) + self._update_matrix() + elif isinstance(stmt, SFParamStmt): + self.scale = (stmt.a, stmt.b) + self._update_matrix() + elif isinstance(stmt, ASParamStmt): + self.axis = 'yx' if stmt.mode == 'AYBX' else 'xy' + self._update_matrix() + elif isinstance(stmt, IRParamStmt): + self.angle = stmt.angle + elif isinstance(stmt, AMParamStmt) and not isinstance(stmt, AMParamStmtEx): + stmt = AMParamStmtEx.from_stmt(stmt) + return (self.TYPE_AM, [stmt]) + elif isinstance(stmt, ADParamStmt) and not isinstance(stmt, AMParamStmtEx): + stmt = ADParamStmtEx.from_stmt(stmt) + return (self.TYPE_AD, [stmt]) + elif isinstance(stmt, CoordStmt): + self._normalize_coordinate(stmt) + + if type(stmt).__name__ in self.ignored_stmt: + return (self.TYPE_NONE, None) + else: + return (self.TYPE_MAIN, [stmt]) + + def _update_matrix(self): + if self.axis == 'xy': + mx = -1 if self.mirror[0] else 1 + my = -1 if self.mirror[1] else 1 + self.matrix = ( + self.scale[0] * mx, self.offset[0], + self.scale[1] * my, self.offset[1], + self.scale[0] * mx, self.scale[1] * my) + else: + mx = -1 if self.mirror[1] else 1 + my = -1 if self.mirror[0] else 1 + self.matrix = ( + self.scale[1] * mx, self.offset[1], + self.scale[0] * my, self.offset[0], + self.scale[1] * mx, self.scale[0] * my) + + def _normalize_coordinate(self, stmt): + if stmt.only_function: + if stmt.function == 'G01' or stmt.function == 'G1': + self.interpolation = self.IP_LINEAR + elif stmt.function == 'G02' or stmt.function == 'G2': + self.interpolation = self.IP_ARC + self.direction = self.DIR_CLOCKWISE + if self.mirror[0] != self.mirror[1]: + self.direction = self.DIR_COUNTERCLOCKWISE + stmt.function = 'G03' + elif stmt.function == 'G03' or stmt.function == 'G3': + self.interpolation = self.IP_ARC + self.direction = self.DIR_COUNTERCLOCKWISE + if self.mirror[0] != self.mirror[1]: + self.direction = self.DIR_CLOCKWISE + stmt.function = 'G02' + return + if self.notation == 'absolute': + x = stmt.x if stmt.x is not None else self.x + y = stmt.y if stmt.y is not None else self.y + else: + x = self.x + stmt.x if stmt.x is not None else 0 + y = self.y + stmt.y if stmt.y is not None else 0 + self.x, self.y = x, y + self.op = stmt.op if stmt.op is not None else self.op + + stmt.op = self.op + stmt.x = self.matrix[0] * x + self.matrix[1] + stmt.y = self.matrix[2] * y + self.matrix[3] + if stmt.op == 'D01' and self.interpolation == self.IP_ARC: + stmt.i = self.matrix[4] * stmt.i if stmt.i is not None else 0 + stmt.j = self.matrix[5] * stmt.j if stmt.j is not None else 0 -- cgit From 4c4ba0762b30fdd4633a6d2868c508184d681b7d Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Mon, 9 Sep 2019 21:52:52 +0900 Subject: fix issue #2: single quadrant mode is supported --- gerberex/rs274x.py | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) (limited to 'gerberex') diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py index fae0b32..42ae17d 100644 --- a/gerberex/rs274x.py +++ b/gerberex/rs274x.py @@ -208,6 +208,7 @@ class GerberContext(FileSettings): 1, 0, 1, 1) + self.in_single_quadrant_mode = False self.op = None self.interpolation = self.IP_LINEAR self.direction = self.DIR_CLOCKWISE @@ -237,6 +238,9 @@ class GerberContext(FileSettings): elif isinstance(stmt, ADParamStmt) and not isinstance(stmt, AMParamStmtEx): stmt = ADParamStmtEx.from_stmt(stmt) return (self.TYPE_AD, [stmt]) + elif isinstance(stmt, QuadrantModeStmt): + self.in_single_quadrant_mode = stmt.mode == 'single-quadrant' + stmt.mode = 'multi-quadrant' elif isinstance(stmt, CoordStmt): self._normalize_coordinate(stmt) @@ -262,22 +266,23 @@ class GerberContext(FileSettings): self.scale[1] * mx, self.scale[0] * my) def _normalize_coordinate(self, stmt): + if stmt.function == 'G01' or stmt.function == 'G1': + self.interpolation = self.IP_LINEAR + elif stmt.function == 'G02' or stmt.function == 'G2': + self.interpolation = self.IP_ARC + self.direction = self.DIR_CLOCKWISE + if self.mirror[0] != self.mirror[1]: + stmt.function = 'G03' + elif stmt.function == 'G03' or stmt.function == 'G3': + self.interpolation = self.IP_ARC + self.direction = self.DIR_COUNTERCLOCKWISE + if self.mirror[0] != self.mirror[1]: + stmt.function = 'G02' if stmt.only_function: - if stmt.function == 'G01' or stmt.function == 'G1': - self.interpolation = self.IP_LINEAR - elif stmt.function == 'G02' or stmt.function == 'G2': - self.interpolation = self.IP_ARC - self.direction = self.DIR_CLOCKWISE - if self.mirror[0] != self.mirror[1]: - self.direction = self.DIR_COUNTERCLOCKWISE - stmt.function = 'G03' - elif stmt.function == 'G03' or stmt.function == 'G3': - self.interpolation = self.IP_ARC - self.direction = self.DIR_COUNTERCLOCKWISE - if self.mirror[0] != self.mirror[1]: - self.direction = self.DIR_CLOCKWISE - stmt.function = 'G02' return + + last_x = self.x + last_y = self.y if self.notation == 'absolute': x = stmt.x if stmt.x is not None else self.x y = stmt.y if stmt.y is not None else self.y @@ -291,5 +296,15 @@ class GerberContext(FileSettings): stmt.x = self.matrix[0] * x + self.matrix[1] stmt.y = self.matrix[2] * y + self.matrix[3] if stmt.op == 'D01' and self.interpolation == self.IP_ARC: - stmt.i = self.matrix[4] * stmt.i if stmt.i is not None else 0 - stmt.j = self.matrix[5] * stmt.j if stmt.j is not None else 0 + qx, qy = 1, 1 + if self.in_single_quadrant_mode: + if self.direction == self.DIR_CLOCKWISE: + qx = 1 if y > last_y else -1 + qy = 1 if x < last_x else -1 + else: + qx = 1 if y < last_y else -1 + qy = 1 if x > last_x else -1 + if last_x == x and last_y == y: + qx, qy = 0, 0 + stmt.i = qx * self.matrix[4] * stmt.i if stmt.i is not None else 0 + stmt.j = qy * self.matrix[5] * stmt.j if stmt.j is not None else 0 -- cgit From 00351ebe277aeb90e7463d1b0bd55402249c4687 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Thu, 12 Sep 2019 23:44:50 +0900 Subject: add IP command handling function --- gerberex/rs274x.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) (limited to 'gerberex') diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py index 42ae17d..3a3a712 100644 --- a/gerberex/rs274x.py +++ b/gerberex/rs274x.py @@ -53,6 +53,8 @@ class GerberFile(gerber.rs274x.GerberFile): self.main_statements.extend(stmts) if self.context.angle != 0: self.rotate(self.context.angle) + if self.context.is_negative: + self.nagate_polarity() self.context.notation = 'absolute' self.context.zeros = 'trailing' @@ -128,6 +130,11 @@ class GerberFile(gerber.rs274x.GerberFile): statement.x = last_rx statement.y = last_ry + def nagate_polarity(self): + for statement in self.main_statements: + if isinstance(statement, LPParamStmt): + statement.lp = 'dark' if statement.lp == 'clear' else 'clear' + def _generalize_aperture(self): RECTANGLE = 0 LANDSCAPE_OBROUND = 1 @@ -208,6 +215,9 @@ class GerberContext(FileSettings): 1, 0, 1, 1) + self.is_negative = False + self.is_first_coordinate = True + self.no_polarity = True self.in_single_quadrant_mode = False self.op = None self.interpolation = self.IP_LINEAR @@ -216,6 +226,7 @@ class GerberContext(FileSettings): self.y = 0. def normalize_statement(self, stmt): + additional_stmts = None if isinstance(stmt, INParamStmt): self.name = stmt.name elif isinstance(stmt, MIParamStmt): @@ -241,14 +252,24 @@ class GerberContext(FileSettings): elif isinstance(stmt, QuadrantModeStmt): self.in_single_quadrant_mode = stmt.mode == 'single-quadrant' stmt.mode = 'multi-quadrant' + elif isinstance(stmt, IPParamStmt): + self.is_negative = stmt.ip == 'negative' + elif isinstance(stmt, LPParamStmt): + self.no_polarity = False elif isinstance(stmt, CoordStmt): self._normalize_coordinate(stmt) + if self.is_first_coordinate: + self.is_first_coordinate = False + if self.no_polarity: + additional_stmts = [LPParamStmt('LP', 'dark'), stmt] if type(stmt).__name__ in self.ignored_stmt: return (self.TYPE_NONE, None) + elif additional_stmts is not None: + return (self.TYPE_MAIN, additional_stmts) else: return (self.TYPE_MAIN, [stmt]) - + def _update_matrix(self): if self.axis == 'xy': mx = -1 if self.mirror[0] else 1 -- cgit From fc3f1a23b87d9c4e51967abb0ed4107daa2be5cf Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sat, 28 Sep 2019 17:40:09 +0900 Subject: improve DXF file handling functions: - DM_LINE mode support to generate Excellon routing sequence - DM_MOUSE_BITES mode support to generate mouse bites along all path also, not only line object --- gerberex/dxf.py | 327 ++++++++++++++++++++------------------------------- gerberex/dxf_path.py | 174 +++++++++++++++++++-------- 2 files changed, 253 insertions(+), 248 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index ae543ae..8f0b984 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -12,7 +12,7 @@ from gerber.gerber_statements import ADParamStmt from gerber.excellon_statements import ExcellonTool from gerber.excellon_statements import CoordinateStmt from gerberex.utility import is_equal_point, is_equal_value -from gerberex.dxf_path import generate_closed_paths +from gerberex.dxf_path import generate_paths from gerberex.excellon import write_excellon_header from gerberex.rs274x import write_gerber_header @@ -25,12 +25,6 @@ class DxfStatement(object): self.end = None self.is_closed = False - def to_gerber(self, settings=None, pitch=0, width=0): - pass - - def to_excellon(self, settings=None, pitch=0, width=0): - pass - def to_inch(self): pass @@ -61,38 +55,6 @@ class DxfLineStatement(DxfStatement): super(DxfLineStatement, self).__init__(entity) self.start = start self.end = end - - def to_gerber(self, settings=FileSettings(), pitch=0, width=0): - if pitch == 0: - x0, y0 = self.start - x1, y1 = self.end - 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) - ) - else: - gstr = "" - for p in self._dots(pitch, width): - gstr += 'X{0}Y{1}D03*\n'.format( - write_gerber_value(p[0], settings.format, - settings.zero_suppression), - write_gerber_value(p[1], settings.format, - settings.zero_suppression)) - return gstr - - def to_excellon(self, settings=FileSettings(), pitch=0, width=0): - if not pitch: - return - gstr = "" - for p in self._dots(pitch, width): - gstr += CoordinateStmt(x=p[0], y=p[1]).to_excellon(settings) + '\n' - return gstr def to_inch(self): self.start = ( @@ -119,7 +81,7 @@ class DxfLineStatement(DxfStatement): self.start = self.end self.end = pt - def _dots(self, pitch, width): + def dots(self, pitch, width, offset=0): x0, y0 = self.start x1, y1 = self.end y1 = self.end[1] @@ -128,13 +90,18 @@ class DxfLineStatement(DxfStatement): l = sqrt(xp * xp + yp * yp) xd = xp * pitch / l yd = yp * pitch / l + x0 += xp * offset / l + y0 += yp * offset / l - d = 0; - while d < l + width / 2: - yield (x0, y0) - x0 += xd - y0 += yd - d += pitch + if offset > l + width / 2: + return (None, offset - l) + else: + d = offset; + while d < l + width / 2: + yield ((x0, y0), d - l) + x0 += xd + y0 += yd + d += pitch def offset(self, offset_x, offset_y): self.start = (self.start[0] + offset_x, self.start[1] + offset_y) @@ -144,104 +111,34 @@ class DxfLineStatement(DxfStatement): self.start = rotate_point(self.start, angle, center) self.end = rotate_point(self.end, angle, center) -class DxfCircleStatement(DxfStatement): - def __init__(self, entity): - super(DxfCircleStatement, self).__init__(entity) - self.radius = self.entity.radius - self.center = (self.entity.center[0], self.entity.center[1]) - self.start = (self.center[0] + self.radius, self.center[1]) - self.end = self.start - self.is_closed = True - - def to_gerber(self, settings=FileSettings(), pitch=0, width=0): - if pitch: - return - r = self.radius - x0, y0 = self.center - 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.radius = inch(self.radius) - self.center = ( - inch(self.center[0]), inch(self.center[1])) - - def to_metric(self): - self.radius = metric(self.radius) - self.center = ( - metric(self.center[0]), metric(self.center[1])) - - def is_equal_to(self, target, error_range=0): - if not isinstance(target, DxfCircleStatement): - return False - return is_equal_point(self.center, target.enter, error_range) and \ - is_equal_value(self.radius, target.radius) - - def reverse(self): - pass - - def offset(self, offset_x, offset_y): - self.center = (self.center[0] + offset_x, self.center[1] + offset_y) - - def rotate(self, angle, center=(0, 0)): - self.center = rotate_point(self.center, angle, center) - class DxfArcStatement(DxfStatement): def __init__(self, entity): super(DxfArcStatement, self).__init__(entity) - self.start_angle = self.entity.start_angle - self.end_angle = self.entity.end_angle - self.radius = self.entity.radius - self.center = (self.entity.center[0], self.entity.center[1]) - self.start = ( - self.center[0] + self.radius * cos(self.start_angle / 180. * pi), - self.center[1] + self.radius * sin(self.start_angle / 180. * pi), - ) - self.end = ( - self.center[0] + self.radius * cos(self.end_angle / 180. * pi), - self.center[1] + self.radius * sin(self.end_angle / 180. * pi), - ) - angle = self.end_angle - self.start_angle - self.is_closed = angle >= 360 or angle <= -360 - - def to_gerber(self, settings=FileSettings(), pitch=0, width=0): - if pitch: - return - x0 = self.center[0] - y0 = self.center[1] - start_x, start_y = self.start - end_x, end_y = self.end - - return 'G01*\nX{0}Y{1}D02*\n' \ - 'G75*\nG{2}*\nX{3}Y{4}I{5}J{6}D01*'.format( - write_gerber_value(start_x, settings.format, - settings.zero_suppression), - write_gerber_value(start_y, settings.format, - settings.zero_suppression), - '02' if self.start_angle > self.end_angle else '03', - write_gerber_value(end_x, settings.format, - settings.zero_suppression), - write_gerber_value(end_y, settings.format, - settings.zero_suppression), - write_gerber_value(x0 - start_x, settings.format, - settings.zero_suppression), - write_gerber_value(y0 - start_y, settings.format, - settings.zero_suppression) - ) + if entity.dxftype == 'CIRCLE': + self.radius = self.entity.radius + self.center = (self.entity.center[0], self.entity.center[1]) + self.start = (self.center[0] + self.radius, self.center[1]) + self.end = self.start + self.start_angle = 0 + self.end_angle = -360 + self.is_closed = True + elif entity.dxftype == 'ARC': + self.start_angle = self.entity.start_angle + self.end_angle = self.entity.end_angle + self.radius = self.entity.radius + self.center = (self.entity.center[0], self.entity.center[1]) + self.start = ( + self.center[0] + self.radius * cos(self.start_angle / 180. * pi), + self.center[1] + self.radius * sin(self.start_angle / 180. * pi), + ) + self.end = ( + self.center[0] + self.radius * cos(self.end_angle / 180. * pi), + self.center[1] + self.radius * sin(self.end_angle / 180. * pi), + ) + angle = self.end_angle - self.start_angle + self.is_closed = angle >= 360 or angle <= -360 + else: + raise Exception('invalid DXF type was specified') def to_inch(self): self.radius = inch(self.radius) @@ -274,6 +171,28 @@ class DxfArcStatement(DxfStatement): self.start = self.end self.end = tmp + def dots(self, pitch, width, offset=0): + angle = self.end_angle - self.start_angle + afactor = 1 if angle > 0 else -1 + aangle = angle * afactor + L = 2 * pi * self.radius + l = L * aangle / 360 + pangle = pitch / L * 360 + wangle = width / L * 360 + oangle = offset / L * 360 + + if offset > l + width / 2: + yield (None, offset - l) + else: + da = oangle + while da < aangle + wangle / 2: + cangle = self.start_angle + da * afactor + x = self.radius * cos(cangle / 180 * pi) + self.center[0] + y = self.radius * sin(cangle / 180 * pi) + self.center[1] + remain = (da - aangle) / 360 * L + yield((x, y), remain) + da += pangle + def offset(self, offset_x, offset_y): self.center = (self.center[0] + offset_x, self.center[1] + offset_y) self.start = (self.start[0] + offset_x, self.start[1] + offset_y) @@ -296,36 +215,30 @@ class DxfPolylineStatement(DxfStatement): else: self.end = (self.entity.points[-1][0], self.entity.points[-1][1]) - def to_gerber(self, settings=FileSettings(), pitch=0, width=0): - if pitch: - return - 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 disassemble(self): + class Item: + pass + def ptseq(): for i in range(1, len(self.entity.points)): yield i if self.entity.is_closed: yield 0 - + + x0 = self.entity.points[0][0] + y0 = self.entity.points[0][1] + b = self.entity.bulge[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), - ) + item = Item() + item.dxftype = 'LINE' + item.start = (x0, y0) + item.end = (x1, y1) + item.is_closed = False + yield DxfLineStatement.from_entity(item) else: ang = 4 * atan(b) xm = x0 + x1 @@ -334,24 +247,27 @@ class DxfPolylineStatement(DxfStatement): 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) - ) + rx0 = x0 - xc + ry0 = y0 - yc + rc = max(min(rx0 / r, 1.0), -1.0) + start_angle = acos(rc) if ry0 > 0 else 2 * pi - acos(rc) + start_angle *= 180 / pi + end_angle = start_angle + ang * 180 / pi + + item = Item() + item.dxftype = 'ARC' + item.start = (x0, y0) + item.end = (x1, y1) + item.start_angle = start_angle + item.end_angle = end_angle + item.radius = r + item.center = (xc, yc) + item.is_closed = end_angle - start_angle >= 360 + yield DxfArcStatement(item) x0 = x1 y0 = y1 b = self.entity.bulge[idx] - - return gerber def to_inch(self): self.start = (inch(self.start[0]), inch(self.start[1])) @@ -376,7 +292,6 @@ class DxfPolylineStatement(DxfStatement): for idx in range(len(self.entity.points)): self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center) - class DxfStatements(object): def __init__(self, statements, units, dcode=10, draw_mode=None): if draw_mode == None: @@ -388,7 +303,7 @@ class DxfStatements(object): self.width = 0 self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR self.statements = statements - self.paths = generate_closed_paths(self.statements, self.error_range) + self.close_paths, self.open_paths = generate_paths(self.statements, self.error_range) @property def units(self): @@ -401,58 +316,62 @@ class DxfStatements(object): 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) - for path in self.paths: + for path in self.close_paths: yield path.to_gerber(settings) yield 'G37*' else: - for statement in self.statements: - yield statement.to_gerber( - settings, - pitch=self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0, - width=self.width) + pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0 + for path in self.open_paths: + yield path.to_gerber(settings, pitch=pitch, width=self.width) + for path in self.close_paths: + yield path.to_gerber(settings, pitch=pitch, width=self.width) return '\n'.join(gerbers()) def to_excellon(self, settings=FileSettings()): - if not self.draw_mode == DxfFile.DM_MOUSE_BITES: + if self.draw_mode == DxfFile.DM_FILL: return def drills(): - for statement in self.statements: - if isinstance(statement, DxfLineStatement): - yield statement.to_excellon(settings, pitch=self.pitch, width=self.width) + pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0 + for path in self.open_paths: + yield path.to_excellon(settings, pitch=pitch, width=self.width) + for path in self.close_paths: + yield path.to_excellon(settings, pitch=pitch, width=self.width) return '\n'.join(drills()) def to_inch(self): if self._units == 'metric': self._units = 'inch' self.pitch = inch(self.pitch) + self.width = inch(self.width) self.error_range = inch(self.error_range) - for statement in self.statements: - statement.to_inch() - for path in self.paths: + for path in self.open_paths: + path.to_inch() + for path in self.close_paths: path.to_inch() def to_metric(self): if self._units == 'inch': self._units = 'metric' self.pitch = metric(self.pitch) + self.width = metric(self.width) self.error_range = metric(self.error_range) - for statement in self.statements: - statement.to_metric() - for path in self.paths: + for path in self.open_paths: + path.to_metric() + for path in self.close_paths: path.to_metric() def offset(self, offset_x, offset_y): - for statement in self.statements: - statement.offset(offset_x, offset_y) + for path in self.open_paths: + path.offset(offset_x, offset_y) + for path in self.close_paths: + path.offset(offset_x, offset_y) def rotate(self, angle, center=(0, 0)): - for statement in self.statements: - statement.rotate(angle, center) + for path in self.open_paths: + path.rotate(angle, center) + for path in self.close_paths: + path.rotate(angle, center) class DxfFile(CamFile): DM_LINE = 0 @@ -483,7 +402,7 @@ class DxfFile(CamFile): elif entity.dxftype == 'LINE': statements.append(DxfLineStatement.from_entity(entity)) elif entity.dxftype == 'CIRCLE': - statements.append(DxfCircleStatement(entity)) + statements.append(DxfArcStatement(entity)) elif entity.dxftype == 'ARC': statements.append(DxfArcStatement(entity)) @@ -513,6 +432,10 @@ class DxfFile(CamFile): self._draw_mode = draw_mode self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0) + if settings.units == 'inch': + self.aperture.to_inch() + else: + self.aperture.to_metric() self.statements = DxfStatements( statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode) diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py index ca48d00..0a92287 100644 --- a/gerberex/dxf_path.py +++ b/gerberex/dxf_path.py @@ -6,10 +6,11 @@ from gerber.utils import inch, metric, write_gerber_value from gerber.cam import FileSettings from gerberex.utility import is_equal_point, is_equal_value +from gerberex.excellon import CoordinateStmtEx class DxfPath(object): - def __init__(self, statement, error_range=0): - self.statements = [statement] + def __init__(self, statements, error_range=0): + self.statements = statements self.error_range = error_range @property @@ -22,8 +23,10 @@ class DxfPath(object): @property def is_closed(self): - return len(self.statements) > 1 and \ - is_equal_point(self.start, self.end, self.error_range) + if len(self.statements) == 1: + return self.statements[0].is_closed + else: + return is_equal_point(self.start, self.end, self.error_range) def is_equal_to(self, target, error_range=0): if not isinstance(target, DxfPath): @@ -43,12 +46,31 @@ class DxfPath(object): return False return True return False + + def contain(self, target, error_range=0): + for statement in self.statements: + if statement.is_equal_to(target, error_range): + return True + else: + return False def to_inch(self): self.error_range = inch(self.error_range) + for statement in self.statements: + statement.to_inch() def to_metric(self): self.error_range = metric(self.error_range) + for statement in self.statements: + statement.to_metric() + + def offset(self, offset_x, offset_y): + for statement in self.statements: + statement.offset(offset_x, offset_y) + + def rotate(self, angle, center=(0, 0)): + for statement in self.statements: + statement.rotate(angle, center) def reverse(self): rlist = [] @@ -133,60 +155,118 @@ class DxfPath(object): def to_gerber(self, settings=FileSettings(), pitch=0, width=0): from gerberex.dxf import DxfArcStatement - if pitch: - return + if pitch == 0: + x0, y0 = self.statements[0].start + 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), + ) - x0, y0 = self.statements[0].start - 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), - ) - - for statement in self.statements: - x0, y0 = statement.start - x1, y1 = statement.end - if isinstance(statement, DxfArcStatement): - xc, yc = statement.center - gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format( - '03' if statement.end_angle > statement.start_angle 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) - ) - else: - gerber += '\nG01*\nX{0}Y{1}D01*'.format( - write_gerber_value(x1, settings.format, - settings.zero_suppression), - write_gerber_value(y1, settings.format, + for statement in self.statements: + x0, y0 = statement.start + x1, y1 = statement.end + if isinstance(statement, DxfArcStatement): + xc, yc = statement.center + gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format( + '03' if statement.end_angle > statement.start_angle 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) + ) + else: + 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: + def ploter(x, y): + return 'X{0}Y{1}D03*\n'.format( + write_gerber_value(x, settings.format, settings.zero_suppression), + write_gerber_value(y, settings.format, + settings.zero_suppression), ) + gerber = self._plot_dots(pitch, width, ploter) return gerber -def generate_closed_paths(statements, error_range=0): - from gerberex.dxf import DxfLineStatement, DxfArcStatement + def to_excellon(self, settings=FileSettings(), pitch=0, width=0): + from gerberex.dxf import DxfArcStatement + if pitch == 0: + x, y = self.statements[0].start + excellon = 'G00{0}\nM15\n'.format( + CoordinateStmtEx(x=x, y=y).to_excellon(settings)) + + for statement in self.statements: + x, y = statement.end + if isinstance(statement, DxfArcStatement): + r = statement.radius + excellon += '{0}{1}\n'.format( + 'G03' if statement.end_angle > statement.start_angle else 'G02', + CoordinateStmtEx(x=x, y=y, radius=r).to_excellon(settings)) + else: + excellon += 'G01{0}\n'.format( + CoordinateStmtEx(x=x, y=y).to_excellon(settings)) + + excellon += 'M16\nG05\n' + else: + def ploter(x, y): + return CoordinateStmtEx(x=x, y=y).to_excellon(settings) + '\n' + excellon = self._plot_dots(pitch, width, ploter) + + return excellon + + def _plot_dots(self, pitch, width, ploter): + out = '' + offset = 0 + for idx in range(0, len(self.statements)): + statement = self.statements[idx] + if offset < 0: + offset += pitch + for dot, offset in statement.dots(pitch, width, offset): + if dot is None: + break + if offset > 0 and (statement.is_closed or idx != len(self.statements) - 1): + break + #if idx == len(self.statements) - 1 and statement.is_closed and offset > -pitch: + # break + out += ploter(dot[0], dot[1]) + return out + + +def generate_paths(statements, error_range=0): + from gerberex.dxf import DxfPolylineStatement + + paths = [] + for statement in filter(lambda s: isinstance(s, DxfPolylineStatement), statements): + units = [unit for unit in statement.disassemble()] + paths.append(DxfPath(units, error_range)) unique_statements = [] redundant = 0 - for statement in statements: - for target in unique_statements: - if not isinstance(statement, DxfLineStatement) and \ - not isinstance(statement, DxfArcStatement): - break - if statement.is_equal_to(target, error_range): + for statement in filter(lambda s: not isinstance(s, DxfPolylineStatement), statements): + for path in paths: + if path.contain(statement): redundant += 1 break else: - unique_statements.append(statement) + for target in unique_statements: + if statement.is_equal_to(target, error_range): + redundant += 1 + break + else: + unique_statements.append(statement) - paths = [DxfPath(s, error_range) for s in unique_statements] + paths.extend([DxfPath([s], error_range) for s in unique_statements]) prev_paths_num = 0 while prev_paths_num != len(paths): @@ -201,5 +281,7 @@ def generate_closed_paths(statements, error_range=0): working.append(mergee) prev_paths_num = len(paths) paths = working - return list(filter(lambda p: p.is_closed, paths)) + closed_path = list(filter(lambda p: p.is_closed, paths)) + open_path = list(filter(lambda p: not p.is_closed, paths)) + return (closed_path, open_path) -- cgit From 48b35377b1cce897a70d1d037b3e73b616af9be7 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sat, 28 Sep 2019 20:42:11 +0900 Subject: minor refactoring --- gerberex/dxf.py | 2 +- gerberex/dxf_path.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 8f0b984..eca246c 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -120,7 +120,7 @@ class DxfArcStatement(DxfStatement): self.start = (self.center[0] + self.radius, self.center[1]) self.end = self.start self.start_angle = 0 - self.end_angle = -360 + self.end_angle = 360 self.is_closed = True elif entity.dxftype == 'ARC': self.start_angle = self.entity.start_angle diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py index 0a92287..1307411 100644 --- a/gerberex/dxf_path.py +++ b/gerberex/dxf_path.py @@ -202,20 +202,22 @@ class DxfPath(object): def to_excellon(self, settings=FileSettings(), pitch=0, width=0): from gerberex.dxf import DxfArcStatement if pitch == 0: - x, y = self.statements[0].start + x0, y0 = self.statements[0].start excellon = 'G00{0}\nM15\n'.format( - CoordinateStmtEx(x=x, y=y).to_excellon(settings)) + CoordinateStmtEx(x=x0, y=y0).to_excellon(settings)) for statement in self.statements: - x, y = statement.end + x0, y0 = statement.start + x1, y1 = statement.end if isinstance(statement, DxfArcStatement): - r = statement.radius + i = statement.center[0] - x0 + j = statement.center[1] - y0 excellon += '{0}{1}\n'.format( 'G03' if statement.end_angle > statement.start_angle else 'G02', - CoordinateStmtEx(x=x, y=y, radius=r).to_excellon(settings)) + CoordinateStmtEx(x=x1, y=y1, i=i, j=j).to_excellon(settings)) else: excellon += 'G01{0}\n'.format( - CoordinateStmtEx(x=x, y=y).to_excellon(settings)) + CoordinateStmtEx(x=x1, y=y1).to_excellon(settings)) excellon += 'M16\nG05\n' else: -- cgit From d7a069324222bb8f69adc9b1c815fc9f3f6a29d6 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Mon, 30 Sep 2019 18:52:17 +0900 Subject: fix a issue that coordinate normalization for excellon is imperfect --- gerberex/composition.py | 4 +++- gerberex/dxf.py | 5 ++--- gerberex/excellon.py | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) (limited to 'gerberex') diff --git a/gerberex/composition.py b/gerberex/composition.py index 634640e..b5dffb1 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -138,7 +138,9 @@ class DrillComposition(Composition): if num == t.number: yield statement.to_excellon(self.settings) yield EndOfProgramStmt().to_excellon() - + + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' with open(path, 'w') as f: gerberex.excellon.write_excellon_header(f, self.settings, self.tools) for statement in statements(): diff --git a/gerberex/dxf.py b/gerberex/dxf.py index eca246c..00b7695 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -475,9 +475,8 @@ class DxfFile(CamFile): self.statements.pitch = value def write(self, filename=None, filetype=FT_RX274X): - if self.settings.notation != 'absolute': - raise Exception('DXF file\'s notation must be absolute ') - + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' filename = filename if filename is not None else self.filename with open(filename, 'w') as f: if filetype == self.FT_RX274X: diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 4f867be..570f94e 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -200,6 +200,8 @@ class ExcellonFileEx(ExcellonFile): self.units = 'metric' def write(self, filename=None): + self.notation = 'absolute' + self.zeros = 'trailing' filename = filename if filename is not None else self.filename with open(filename, 'w') as f: write_excellon_header(f, self.settings, [self.tools[t] for t in self.tools]) -- cgit From 244fcaa5346f4fad819cc2b72857cfb2c472944a Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sat, 28 Dec 2019 23:45:33 +0900 Subject: add a function that generate filled gerberdata with representing internal shape by fliping polarity --- gerberex/dxf.py | 282 +++++++++++++++++++++++++++++++++++++++++++++++++-- gerberex/dxf_path.py | 125 ++++++++++++++++++++++- gerberex/utility.py | 9 +- 3 files changed, 406 insertions(+), 10 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 00b7695..95a3114 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -12,12 +12,88 @@ from gerber.gerber_statements import ADParamStmt from gerber.excellon_statements import ExcellonTool from gerber.excellon_statements import CoordinateStmt from gerberex.utility import is_equal_point, is_equal_value -from gerberex.dxf_path import generate_paths +from gerberex.dxf_path import generate_paths, judge_containment from gerberex.excellon import write_excellon_header from gerberex.rs274x import write_gerber_header ACCEPTABLE_ERROR = 0.001 +def _normalize_angle(start_angle, end_angle): + angle = end_angle - start_angle + if angle > 0: + start = start_angle % 360 + else: + angle = -angle + start = end_angle % 360 + angle = min(angle, 360) + start = start - 360 if start > 180 else start + + regions = [] + while angle > 0: + end = start + angle + if end <= 180: + regions.append((start * pi / 180, end * pi / 180)) + angle = 0 + else: + regions.append((start * pi / 180, pi)) + angle = end - 180 + start = -180 + return regions + +def _intersections_of_line_and_circle(start, end, center, radius, error_range): + x1 = start[0] - center[0] + y1 = start[1] - center[1] + x2 = end[0] - center[0] + y2 = end[1] - center[1] + + dx = x2 - x1 + dy = y2 - y1 + dr = sqrt(dx * dx + dy * dy) + D = x1 * y2 - x2 * y1 + + D2 = D * D + dr2 = dr * dr + r2 = radius * radius + delta = r2 * dr2 - D2 + e4 = error_range * error_range * error_range * error_range * 10 + if delta > - e4 and delta < e4: + delta = 0 + if delta < 0: + return None + + sqrt_D = sqrt(delta) + E_x = -dx * sqrt_D if dy < 0 else dx * sqrt_D + E_y = abs(dy) * sqrt_D + + p1_x = (D * dy + E_x) / dr2 + p2_x = (D * dy - E_x) / dr2 + p1_y = (-D * dx + E_y) / dr2 + p2_y = (-D * dx - E_y) / dr2 + + p1_angle = atan2(p1_y, p1_x) + p2_angle = atan2(p2_y, p2_x) + if dx == 0: + p1_t = (p1_y - y1) / dy + p2_t = (p2_y - y1) / dy + else: + p1_t = (p1_x - x1) / dx + p2_t = (p2_x - x1) / dx + + if delta == 0: + return ( + (p1_x + center[0], p1_y + center[1]), + None, + p1_angle, None, + p1_t, None + ) + else: + return ( + (p1_x + center[0], p1_y + center[1]), + (p2_x + center[0], p2_y + center[1]), + p1_angle, p2_angle, + p1_t, p2_t + ) + class DxfStatement(object): def __init__(self, entity): self.entity = entity @@ -51,6 +127,13 @@ class DxfLineStatement(DxfStatement): end = (entity.end[0], entity.end[1]) return cls(entity, start, end) + @property + def bounding_box(self): + return (min(self.start[0], self.end[0]), + min(self.start[1], self.end[1]), + max(self.start[0], self.end[0]), + max(self.start[1], self.end[1])) + def __init__(self, entity, start, end): super(DxfLineStatement, self).__init__(entity) self.start = start @@ -110,6 +193,53 @@ class DxfLineStatement(DxfStatement): def rotate(self, angle, center=(0, 0)): self.start = rotate_point(self.start, angle, center) self.end = rotate_point(self.end, angle, center) + + def intersections_with_halfline(self, point_from, point_to, error_range): + denominator = (self.end[0] - self.start[0]) * (point_to[1] - point_from[1]) - \ + (self.end[1] - self.start[1]) * (point_to[0] - point_from[0]) + de = error_range * error_range + if denominator > -de and denominator < de: + return [] + from_dx = point_from[0] - self.start[0] + from_dy = point_from[1] - self.start[1] + r = ((point_to[1] - point_from[1]) * from_dx - + (point_to[0] - point_from[0]) * from_dy) / denominator + s = ((self.end[1] - self.start[1]) * from_dx - + (self.end[0] - self.start[0]) * from_dy) / denominator + dx = (self.end[0] - self.start[0]) + dy = (self.end[1] - self.start[1]) + le = error_range / sqrt(dx * dx + dy * dy) + if s < 0 or r < -le or r > 1 + le: + return [] + + pt = (self.start[0] + (self.end[0] - self.start[0]) * r, + self.start[1] + (self.end[1] - self.start[1]) * r) + if is_equal_point(pt, self.start, error_range): + return [] + else: + return [pt] + + def intersections_with_arc(self, center, radius, angle_regions, error_range): + intersection = \ + _intersections_of_line_and_circle(self.start, self.end, center, radius, error_range) + if intersection is None: + return [] + else: + p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection + + pts = [] + if p1_t >= 0 and p1_t <= 1: + for region in angle_regions: + if p1_angle >= region[0] and p1_angle <= region[1]: + pts.append(p1) + break + if p2 is not None and p2_t >= 0 and p2_t <= 1: + for region in angle_regions: + if p2_angle >= region[0] and p2_angle <= region[1]: + pts.append(p2) + break + + return pts class DxfArcStatement(DxfStatement): def __init__(self, entity): @@ -139,6 +269,12 @@ class DxfArcStatement(DxfStatement): self.is_closed = angle >= 360 or angle <= -360 else: raise Exception('invalid DXF type was specified') + self.angle_regions = _normalize_angle(self.start_angle, self.end_angle) + + @property + def bounding_box(self): + return (self.center[0] - self.radius, self.center[1] - self.radius, + self.center[0] + self.radius, self.center[1] + self.radius) def to_inch(self): self.radius = inch(self.radius) @@ -204,6 +340,82 @@ class DxfArcStatement(DxfStatement): self.center = rotate_point(self.center, angle, center) self.start = rotate_point(self.start, angle, center) self.end = rotate_point(self.end, angle, center) + self.angle_regions = _normalize_angle(self.start_angle, self.end_angle) + + def intersections_with_halfline(self, point_from, point_to, error_range): + intersection = \ + _intersections_of_line_and_circle( + point_from, point_to, self.center, self.radius, error_range) + if intersection is None: + return [] + else: + p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection + + if is_equal_point(p1, self.start, error_range): + p1 = None + elif p2 is not None and is_equal_point(p2, self.start, error_range): + p2 = None + + aerror = error_range * self.radius + pts = [] + if p1 is not None and p1_t >= 0 and not is_equal_point(p1, self.start, error_range): + for region in self.angle_regions: + if p1_angle >= region[0] - aerror and p1_angle <= region[1] + aerror: + pts.append(p1) + break + if p2 is not None and p2_t >= 0 and not is_equal_point(p2, self.start, error_range): + for region in self.angle_regions: + if p2_angle >= region[0] - aerror and p2_angle <= region[1] + aerror: + pts.append(p2) + break + + return pts + + def intersections_with_arc(self, center, radius, angle_regions, error_range): + x1 = center[0] - self.center[0] + y1 = center[1] - self.center[1] + r1 = self.radius + r2 = radius + cd_sq = x1 * x1 + y1 * y1 + cd = sqrt(cd_sq) + rd = abs(r1 - r2) + + if (cd >= 0 and cd <= rd) or cd >= r1 + r2: + return [] + + A = (cd_sq + r1 * r1 - r2 * r2) / 2 + scale = sqrt(cd_sq * r1 * r1 - A * A) / cd_sq + xl = A * x1 / cd_sq + xr = y1 * scale + yl = A * y1 / cd_sq + yr = x1 * scale + + pt1_x = xl + xr + pt1_y = yl - yr + pt2_x = xl - xr + pt2_y = yl + yr + pt1_angle1 = atan2(pt1_y, pt1_x) + pt1_angle2 = atan2(pt1_y - y1, pt1_x - x1) + pt2_angle1 = atan2(pt2_y, pt2_x) + pt2_angle2 = atan2(pt2_y - y1, pt2_x - x1) + + aerror = error_range * self.radius + pts=[] + for region in self.angle_regions: + if pt1_angle1 >= region[0] and pt1_angle1 <= region[1]: + for region in angle_regions: + if pt1_angle2 >= region[0] - aerror and pt1_angle2 <= region[1] + aerror: + pts.append((pt1_x + self.center[0], pt1_y + self.center[1])) + break + break + for region in self.angle_regions: + if pt2_angle1 >= region[0] and pt2_angle1 <= region[1]: + for region in angle_regions: + if pt2_angle2 >= region[0] - aerror and pt2_angle2 <= region[1] + aerror: + pts.append((pt2_x + self.center[0], pt2_y + self.center[1])) + break + break + return pts class DxfPolylineStatement(DxfStatement): def __init__(self, entity): @@ -293,31 +505,69 @@ class DxfPolylineStatement(DxfStatement): self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center) class DxfStatements(object): - def __init__(self, statements, units, dcode=10, draw_mode=None): - if draw_mode == None: + def __init__(self, statements, units, dcode=10, draw_mode=None, fill_mode=None): + if draw_mode is None: draw_mode = DxfFile.DM_LINE + if fill_mode is None: + fill_mode = DxfFile.FM_TURN_OVER self._units = units self.dcode = dcode self.draw_mode = draw_mode + self.fill_mode = fill_mode self.pitch = inch(1) if self._units == 'inch' else 1 self.width = 0 self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR - self.statements = statements + self.statements = list(filter( + lambda i: not (isinstance(i, DxfLineStatement) and \ + is_equal_point(i.start, i.end, self.error_range)), + statements + )) self.close_paths, self.open_paths = generate_paths(self.statements, self.error_range) + self.sorted_close_paths = [] + self.polarity = True # True means dark, False means clear @property def units(self): return _units + def _polarity_command(self, polarity=None): + if polarity is None: + polarity = self.polarity + return '%LPD*%' if polarity else '%LPC*%' + + def _prepare_sorted_close_paths(self): + if self.sorted_close_paths: + return + for i in range(0, len(self.close_paths)): + for j in range(i + 1, len(self.close_paths)): + containee, container = judge_containment( + self.close_paths[i], self.close_paths[j], self.error_range) + if containee is not None: + containee.containers.append(container) + self.sorted_close_paths = sorted(self.close_paths, key=lambda path: len(path.containers)) + def to_gerber(self, settings=FileSettings()): def gerbers(): yield 'G75*' - yield '%LPD*%' + yield self._polarity_command() yield 'D{0}*'.format(self.dcode) if self.draw_mode == DxfFile.DM_FILL: yield 'G36*' - for path in self.close_paths: - yield path.to_gerber(settings) + if self.fill_mode == DxfFile.FM_TURN_OVER: + self._prepare_sorted_close_paths() + polarity = self.polarity + level = 0 + for path in self.sorted_close_paths: + if len(path.containers) > level: + level = len(path.containers) + polarity = not polarity + yield 'G37*' + yield self._polarity_command(polarity) + yield 'G36*' + yield path.to_gerber(settings) + else: + for path in self.close_paths: + yield path.to_gerber(settings) yield 'G37*' else: pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0 @@ -378,6 +628,9 @@ class DxfFile(CamFile): DM_FILL = 1 DM_MOUSE_BITES = 2 + FM_SIMPLE = 0 + FM_TURN_OVER = 1 + FT_RX274X = 0 FT_EXCELLON = 1 @@ -430,6 +683,7 @@ class DxfFile(CamFile): super(DxfFile, self).__init__(settings=settings, filename=filename) self._draw_mode = draw_mode + self._fill_mode = self.FM_TURN_OVER self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0) if settings.units == 'inch': @@ -437,7 +691,7 @@ class DxfFile(CamFile): else: self.aperture.to_metric() self.statements = DxfStatements( - statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode) + statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode, fill_mode=self.filename) @property def dcode(self): @@ -466,6 +720,15 @@ class DxfFile(CamFile): self._draw_mode = value self.statements.draw_mode = value + @property + def fill_mode(self): + return self._fill_mode + + @fill_mode.setter + def fill_mode(self, value): + self._fill_mode = value + self.statements.fill_mode = value + @property def pitch(self): return self.statements.pitch @@ -512,6 +775,9 @@ class DxfFile(CamFile): def rotate(self, angle, center=(0, 0)): self.statements.rotate(angle, center) + def negate_polarity(self): + self.statements.polarity = not self.statements.polarity + def loads(data, filename=None): if sys.version_info.major == 2: data = unicode(data) diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py index 1307411..bb620ff 100644 --- a/gerberex/dxf_path.py +++ b/gerberex/dxf_path.py @@ -5,13 +5,17 @@ from gerber.utils import inch, metric, write_gerber_value from gerber.cam import FileSettings -from gerberex.utility import is_equal_point, is_equal_value +from gerberex.utility import is_equal_point, is_equal_value, normalize_vec2d, dot_vec2d from gerberex.excellon import CoordinateStmtEx class DxfPath(object): def __init__(self, statements, error_range=0): self.statements = statements self.error_range = error_range + self.bounding_box = statements[0].bounding_box + self.containers = [] + for statement in statements[1:]: + self._merge_bounding_box(statement.bounding_box) @property def start(self): @@ -116,12 +120,15 @@ class DxfPath(object): if j > 0: del mergee[-j] del self.statements[0:j] + for statement in mergee: + self._merge_bounding_box(statement.bounding_box) self.statements.extend(mergee) return True else: if self.statements[-1].is_equal_to(element, error_range) or \ self.statements[0].is_equal_to(element, error_range): return False + self._merge_bounding_box(element.bounding_box) self.statements.appen(element) return True @@ -153,6 +160,21 @@ class DxfPath(object): self.statements.insert(0, element) return True + def _merge_bounding_box(self, box): + self.bounding_box = (min(self.bounding_box[0], box[0]), + min(self.bounding_box[1], box[1]), + max(self.bounding_box[2], box[2]), + max(self.bounding_box[3], box[3])) + + def may_be_in_collision(self, path): + if self.bounding_box[0] >= path.bounding_box[2] or \ + self.bounding_box[1] >= path.bounding_box[3] or \ + self.bounding_box[2] <= path.bounding_box[0] or \ + self.bounding_box[3] <= path.bounding_box[1]: + return False + else: + return True + def to_gerber(self, settings=FileSettings(), pitch=0, width=0): from gerberex.dxf import DxfArcStatement if pitch == 0: @@ -244,7 +266,61 @@ class DxfPath(object): out += ploter(dot[0], dot[1]) return out + def intersections_with_halfline(self, point_from, point_to, error_range=0): + def calculator(statement): + return statement.intersections_with_halfline(point_from, point_to, error_range) + def validator(pt, statement, idx): + if is_equal_point(pt, statement.end, error_range) and \ + not self._judge_cross(point_from, point_to, idx, error_range): + return False + return True + return self._collect_intersections(calculator, validator, error_range) + + def intersections_with_arc(self, center, radius, angle_regions, error_range=0): + def calculator(statement): + return statement.intersections_with_arc(center, radius, angle_regions, error_range) + return self._collect_intersections(calculator, None, error_range) + def _collect_intersections(self, calculator, validator, error_range): + allpts = [] + last = allpts + for i in range(0, len(self.statements)): + statement = self.statements[i] + cur = calculator(statement) + if cur: + for pt in cur: + for dest in allpts: + if is_equal_point(pt, dest, error_range): + break + else: + if validator is not None and not validator(pt, statement, i): + continue + allpts.append(pt) + last = cur + return allpts + + def _judge_cross(self, from_pt, to_pt, index, error_range): + standard = normalize_vec2d((to_pt[0] - from_pt[0], to_pt[1] - from_pt[1])) + normal = (standard[1], standard[0]) + def statements(): + for i in range(index, len(self.statements)): + yield self.statements[i] + for i in range(0, index): + yield self.statements[i] + dot_standard = None + for statement in statements(): + tstart = statement.start + tend = statement.end + target = normalize_vec2d((tend[0] - tstart[0], tend[1] - tstart[1])) + dot= dot_vec2d(normal, target) + if dot_standard is None: + dot_standard = dot + continue + if is_equal_point(standard, target, error_range): + continue + return (dot_standard > 0 and dot > 0) or (dot_standard < 0 and dot < 0) + raise Exception('inconsistensy is detected while cross judgement between paths') + def generate_paths(statements, error_range=0): from gerberex.dxf import DxfPolylineStatement @@ -287,3 +363,50 @@ def generate_paths(statements, error_range=0): closed_path = list(filter(lambda p: p.is_closed, paths)) open_path = list(filter(lambda p: not p.is_closed, paths)) return (closed_path, open_path) + +def judge_containment(path1, path2, error_range=0): + from gerberex.dxf import DxfArcStatement, DxfLineStatement + + nocontainment = (None, None) + if not path1.may_be_in_collision(path2): + return nocontainment + + def is_in_line_segment(point_from, point_to, point): + dx = point_to[0] - point_from[0] + ratio = (point[0] - point_from[0]) / dx if dx != 0 else \ + (point[1] - point_from[1]) / (point_to[1] - point_from[1]) + return ratio >= 0 and ratio <= 1 + + def contain_in_path(statement, path): + if isinstance(statement, DxfLineStatement): + segment = (statement.start, statement.end) + elif isinstance(statement, DxfArcStatement): + if statement.start == statement.end: + segment = (statement.start, statement.center) + else: + segment = (statement.start, statement.end) + else: + raise Exception('invalid dxf statement type') + pts = path.intersections_with_halfline(segment[0], segment[1], error_range) + if len(pts) % 2 == 0: + return False + for pt in pts: + if is_in_line_segment(segment[0], segment[1], pt): + return False + if isinstance(statement, DxfArcStatement): + pts = path.intersections_with_arc( + statement.center, statement.radius, statement.angle_regions, error_range) + if len(pts) > 0: + return False + return True + + if contain_in_path(path1.statements[0], path2): + containment = [path1, path2] + elif contain_in_path(path2.statements[0], path1): + containment = [path2, path1] + else: + return nocontainment + for i in range(1, len(containment[0].statements)): + if not contain_in_path(containment[0].statements[i], containment[1]): + return nocontainment + return containment diff --git a/gerberex/utility.py b/gerberex/utility.py index 4c89fa6..37de5e8 100644 --- a/gerberex/utility.py +++ b/gerberex/utility.py @@ -3,7 +3,7 @@ # Copyright 2019 Hiroshi Murayama -from math import cos, sin, pi +from math import cos, sin, pi, sqrt def rotate(x, y, angle, center): x0 = x - center[0] @@ -18,3 +18,10 @@ def is_equal_value(a, b, error_range=0): def is_equal_point(a, b, error_range=0): return is_equal_value(a[0], b[0], error_range) and \ is_equal_value(a[1], b[1], error_range) + +def normalize_vec2d(vec): + length = sqrt(vec[0] * vec[0] + vec[1] * vec[1]) + return (vec[0] / length, vec[1] / length) + +def dot_vec2d(vec1, vec2): + return vec1[0] * vec2[0] + vec1[1] * vec2[1] \ No newline at end of file -- cgit From ca23fbd9534ab3cba3fd7b032816766c1150ebf9 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Mon, 30 Dec 2019 17:51:48 +0900 Subject: fix bugs that fail judgement of path's containment --- gerberex/dxf.py | 20 +++++++++++++++----- gerberex/dxf_path.py | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) (limited to 'gerberex') diff --git a/gerberex/dxf.py b/gerberex/dxf.py index 95a3114..2341092 100644 --- a/gerberex/dxf.py +++ b/gerberex/dxf.py @@ -51,12 +51,13 @@ def _intersections_of_line_and_circle(start, end, center, radius, error_range): dr = sqrt(dx * dx + dy * dy) D = x1 * y2 - x2 * y1 + distance = abs(dy * x1 - dx * y1) / dr + D2 = D * D dr2 = dr * dr r2 = radius * radius delta = r2 * dr2 - D2 - e4 = error_range * error_range * error_range * error_range * 10 - if delta > - e4 and delta < e4: + if distance > radius - error_range and distance < radius + error_range: delta = 0 if delta < 0: return None @@ -198,7 +199,7 @@ class DxfLineStatement(DxfStatement): denominator = (self.end[0] - self.start[0]) * (point_to[1] - point_from[1]) - \ (self.end[1] - self.start[1]) * (point_to[0] - point_from[0]) de = error_range * error_range - if denominator > -de and denominator < de: + if denominator >= -de and denominator <= de: return [] from_dx = point_from[0] - self.start[0] from_dy = point_from[1] - self.start[1] @@ -356,16 +357,25 @@ class DxfArcStatement(DxfStatement): elif p2 is not None and is_equal_point(p2, self.start, error_range): p2 = None + def is_contained(angle, region, error): + if angle >= region[0] - error and angle <= region[1] + error: + return True + if angle < 0 and region[1] > 0: + angle = angle + 2 * pi + elif angle > 0 and region[0] < 0: + angle = angle - 2 * pi + return angle >= region[0] - error and angle <= region[1] + error + aerror = error_range * self.radius pts = [] if p1 is not None and p1_t >= 0 and not is_equal_point(p1, self.start, error_range): for region in self.angle_regions: - if p1_angle >= region[0] - aerror and p1_angle <= region[1] + aerror: + if is_contained(p1_angle, region, aerror): pts.append(p1) break if p2 is not None and p2_t >= 0 and not is_equal_point(p2, self.start, error_range): for region in self.angle_regions: - if p2_angle >= region[0] - aerror and p2_angle <= region[1] + aerror: + if is_contained(p2_angle, region, aerror): pts.append(p2) break diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py index bb620ff..960b054 100644 --- a/gerberex/dxf_path.py +++ b/gerberex/dxf_path.py @@ -301,7 +301,7 @@ class DxfPath(object): def _judge_cross(self, from_pt, to_pt, index, error_range): standard = normalize_vec2d((to_pt[0] - from_pt[0], to_pt[1] - from_pt[1])) - normal = (standard[1], standard[0]) + normal = (standard[1], -standard[0]) def statements(): for i in range(index, len(self.statements)): yield self.statements[i] -- cgit From 3f9295b9d0c2142c46aa238836debdd472811899 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 6 Feb 2021 09:38:22 +0100 Subject: Fix support for comments in aperture definitions --- gerberex/am_expression.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'gerberex') diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py index 5400130..f43ba2e 100644 --- a/gerberex/am_expression.py +++ b/gerberex/am_expression.py @@ -49,6 +49,8 @@ class AMConstantExpression(AMExpression): return self def to_gerber(self, settings=None): + if isinstance(self._value, str): + return self._value gerber = '%.6g' % self._value return '%.6f' % self._value if 'e' in gerber else gerber -- cgit From 71c371ca680483aa9ef18d2998832460dd43abdf Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 6 Feb 2021 09:38:49 +0100 Subject: Fix mystery crashes in excellon parser I don't know this code and I don't get what was intended here, but it makes way more sense to me this way. --- gerberex/excellon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerberex') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 570f94e..f7787d3 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -114,8 +114,8 @@ class ExcellonFileEx(ExcellonFile): if isinstance(stmt, ToolSelectionStmt): current_tool = file.tools[stmt.tool] elif isinstance(stmt, DrillModeStmt): - rout = make_rout(status, rout_statements) - rout_statements = [] + rout = make_rout(status, rout_nodes) + rout_nodes = [] if rout is not None: yield rout status = STAT_DRILL @@ -137,7 +137,7 @@ class ExcellonFileEx(ExcellonFile): status = STAT_ROUT_DOWN elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt): rout = make_rout(status, rout_nodes) - rout_statements = [] + rout_nodes = [] if rout is not None: yield rout status = STAT_ROUT_UP -- cgit