From 4eb0e063bcd34c21b737023aa6ed5baed80658d1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 13 Jun 2021 15:00:17 +0200 Subject: Repo re-org, make gerberex tests run --- gerbonara/gerber/panelize/__init__.py | 8 + gerbonara/gerber/panelize/am_expression.py | 184 ++++++ gerbonara/gerber/panelize/am_primitive.py | 448 ++++++++++++++ gerbonara/gerber/panelize/common.py | 41 ++ gerbonara/gerber/panelize/composition.py | 192 ++++++ gerbonara/gerber/panelize/dxf.py | 796 +++++++++++++++++++++++++ gerbonara/gerber/panelize/dxf_path.py | 412 +++++++++++++ gerbonara/gerber/panelize/excellon.py | 404 +++++++++++++ gerbonara/gerber/panelize/gerber_statements.py | 115 ++++ gerbonara/gerber/panelize/rs274x.py | 331 ++++++++++ gerbonara/gerber/panelize/utility.py | 27 + 11 files changed, 2958 insertions(+) create mode 100644 gerbonara/gerber/panelize/__init__.py create mode 100644 gerbonara/gerber/panelize/am_expression.py create mode 100644 gerbonara/gerber/panelize/am_primitive.py create mode 100644 gerbonara/gerber/panelize/common.py create mode 100644 gerbonara/gerber/panelize/composition.py create mode 100644 gerbonara/gerber/panelize/dxf.py create mode 100644 gerbonara/gerber/panelize/dxf_path.py create mode 100644 gerbonara/gerber/panelize/excellon.py create mode 100644 gerbonara/gerber/panelize/gerber_statements.py create mode 100644 gerbonara/gerber/panelize/rs274x.py create mode 100644 gerbonara/gerber/panelize/utility.py (limited to 'gerbonara/gerber/panelize') diff --git a/gerbonara/gerber/panelize/__init__.py b/gerbonara/gerber/panelize/__init__.py new file mode 100644 index 0000000..a3de2c1 --- /dev/null +++ b/gerbonara/gerber/panelize/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from .common import read, loads, rectangle +from .composition import GerberComposition, DrillComposition +from .dxf import DxfFile diff --git a/gerbonara/gerber/panelize/am_expression.py b/gerbonara/gerber/panelize/am_expression.py new file mode 100644 index 0000000..9e19f71 --- /dev/null +++ b/gerbonara/gerber/panelize/am_expression.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..utils import * +from ..am_eval import OpCode +from ..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): + if isinstance(self._value, str): + return 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)] + +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/gerbonara/gerber/panelize/am_primitive.py b/gerbonara/gerber/panelize/am_primitive.py new file mode 100644 index 0000000..123f030 --- /dev/null +++ b/gerbonara/gerber/panelize/am_primitive.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..utils import * +from ..am_statements import * +from ..am_eval import OpCode + +from .am_expression import eval_macro, AMConstantExpression, AMOperatorExpression + +class AMPrimitiveDef(AMPrimitive): + 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 + + 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].value == 1 else 'off' + diameter = modifiers[1] + center_x = modifiers[2] + center_y = modifiers[3] + 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): + 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].value == 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_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_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), + 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].value == 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 = int(modifiers[1].value + 1) + code = code + exposure = 'on' if modifiers[0].value == 1 else 'off' + 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): + super(AMOutlinePrimitiveDef, self).__init__(code, exposure, rotation) + self.addrs = addrs + + def to_inch(self): + self.addrs = [i.to_inch().optimize() for i in self.addrs] + + def to_metric(self): + self.addrs = [i.to_metric().optimize() 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].value == 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_metric().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 rotate(self, angle, center=None): + pass + +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) diff --git a/gerbonara/gerber/panelize/common.py b/gerbonara/gerber/panelize/common.py new file mode 100644 index 0000000..03bf9b0 --- /dev/null +++ b/gerbonara/gerber/panelize/common.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +import os +from ..common import loads as loads_org +from ..exceptions import ParseError +from ..utils import detect_file_format +from .. import rs274x +from .. import ipc356 + +from . import rs274x as ex_rs274x +from . import excellon +from . import 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 dxf.loads(data, filename) + + fmt = detect_file_format(data) + if fmt == 'rs274x': + file = ex_rs274x.loads(data, filename=filename) + return ex_rs274x.GerberFile.from_gerber_file(file) + elif fmt == 'excellon': + return 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') + + +def rectangle(width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None): + return dxf.DxfFile.rectangle( + width, height, left, bottom, units, draw_mode, filename) diff --git a/gerbonara/gerber/panelize/composition.py b/gerbonara/gerber/panelize/composition.py new file mode 100644 index 0000000..619a0cf --- /dev/null +++ b/gerbonara/gerber/panelize/composition.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama +import os +from functools import reduce +from ..cam import FileSettings +from ..gerber_statements import EofStmt +from ..excellon_statements import * +from ..excellon import DrillSlot, DrillHit +from . import rs274x +from . import excellon +from . import 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.aperture_macros = {} + self.apertures = [] + self.drawings = [] + + def merge(self, file): + if isinstance(file, rs274x.GerberFile): + self._merge_gerber(file) + elif isinstance(file, dxf.DxfFile): + self._merge_dxf(file) + else: + raise Exception('unsupported file type') + + def dump(self, path): + def statements(): + 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() + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' + with open(path, 'w') as f: + rs274x.write_gerber_header(f, self.settings) + for statement in statements(): + f.write(statement.to_gerber(self.settings) + '\n') + + def _merge_gerber(self, file): + aperture_macro_map = {} + aperture_map = {} + + if self.settings: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + 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.context + + 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 + + + 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.tools = [] + self.hits = [] + self.dxf_statements = [] + + def merge(self, file): + if isinstance(file, excellon.ExcellonFileEx): + self._merge_excellon(file) + elif isinstance(file, DxfFile): + self._merge_dxf(file) + else: + raise Exception('unsupported file type') + + def dump(self, path): + def statements(): + 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 h.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() + + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' + with open(path, 'w') as f: + excellon.write_excellon_header(f, self.settings, self.tools) + 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() + + 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 _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() + + 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): + 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/gerbonara/gerber/panelize/dxf.py b/gerbonara/gerber/panelize/dxf.py new file mode 100644 index 0000000..9eb9217 --- /dev/null +++ b/gerbonara/gerber/panelize/dxf.py @@ -0,0 +1,796 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +import io, sys +from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt +import dxfgrabber +from ..cam import CamFile, FileSettings +from ..utils import inch, metric, write_gerber_value, rotate_point +from ..gerber_statements import ADParamStmt +from ..excellon_statements import ExcellonTool +from ..excellon_statements import CoordinateStmt +from .utility import is_equal_point, is_equal_value +from .dxf_path import generate_paths, judge_containment +from .excellon import write_excellon_header +from .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 + + distance = abs(dy * x1 - dx * y1) / dr + + D2 = D * D + dr2 = dr * dr + r2 = radius * radius + delta = r2 * dr2 - D2 + if distance > radius - error_range and distance < radius + error_range: + 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 + self.start = None + self.end = None + self.is_closed = False + + def to_inch(self): + pass + + def to_metric(self): + pass + + def is_equal_to(self, target, error_range=0): + return False + + 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): + start = (entity.start[0], entity.start[1]) + 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 + self.end = end + + def to_inch(self): + 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.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, 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, offset=0): + x0, y0 = self.start + x1, y1 = self.end + y1 = self.end[1] + xp = x1 - x0 + yp = y1 - y0 + l = sqrt(xp * xp + yp * yp) + xd = xp * pitch / l + yd = yp * pitch / l + x0 += xp * offset / l + y0 += yp * offset / l + + 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) + 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) + + 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): + super(DxfArcStatement, self).__init__(entity) + 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') + 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) + 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.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 + + 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) + 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) + 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 + + 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 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 is_contained(p2_angle, region, 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): + 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.end = (self.entity.points[-1][0], self.entity.points[-1][1]) + + 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: + 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 + 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)) + 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] + + 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])) + + 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])) + + 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, 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 = 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 self._polarity_command() + yield 'D{0}*'.format(self.dcode) + if self.draw_mode == DxfFile.DM_FILL: + yield 'G36*' + 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 + 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 self.draw_mode == DxfFile.DM_FILL: + return + def drills(): + 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 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 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 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 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 + DM_FILL = 1 + DM_MOUSE_BITES = 2 + + FM_SIMPLE = 0 + FM_TURN_OVER = 1 + + FT_RX274X = 0 + FT_EXCELLON = 1 + + @classmethod + def from_dxf(cls, dxf, settings=None, draw_mode=None, filename=None): + fsettings = settings if settings else \ + FileSettings(zero_suppression='leading') + + if dxf.header['$INSUNITS'] == 1: + 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(DxfArcStatement(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 = 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 + self._fill_mode = self.FM_TURN_OVER + + 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, fill_mode=self.filename) + + @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),],) + self.statements.width = 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 + + @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 + + @pitch.setter + def pitch(self, value): + self.statements.pitch = value + + def write(self, filename=None, filetype=FT_RX274X): + 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: + 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') + else: + 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') + + + def to_inch(self): + if self.units == 'metric': + self.aperture.to_inch() + self.statements.to_inch() + self.pitch = inch(self.pitch) + self.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + self.aperture.to_metric() + self.statements.to_metric() + self.pitch = metric(self.pitch) + self.units = 'metric' + + 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 negate_polarity(self): + self.statements.polarity = not self.statements.polarity + +def loads(data, filename=None): + if sys.version_info.major == 2: + data = unicode(data) + stream = io.StringIO(data) + dxf = dxfgrabber.read(stream) + return DxfFile.from_dxf(dxf) diff --git a/gerbonara/gerber/panelize/dxf_path.py b/gerbonara/gerber/panelize/dxf_path.py new file mode 100644 index 0000000..201dcff --- /dev/null +++ b/gerbonara/gerber/panelize/dxf_path.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..utils import inch, metric, write_gerber_value +from ..cam import FileSettings +from .utility import is_equal_point, is_equal_value, normalize_vec2d, dot_vec2d +from .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): + return self.statements[0].start + + @property + def end(self): + return self.statements[-1].end + + @property + def is_closed(self): + 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): + 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 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 = [] + for statement in reversed(self.statements): + statement.reverse() + rlist.append(statement) + self.statements = rlist + + def merge(self, element, error_range=0): + 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] + 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 + + 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 _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 .dxf import DxfArcStatement + 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), + ) + + 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 to_excellon(self, settings=FileSettings(), pitch=0, width=0): + from .dxf import DxfArcStatement + if pitch == 0: + x0, y0 = self.statements[0].start + excellon = 'G00{0}\nM15\n'.format( + CoordinateStmtEx(x=x0, y=y0).to_excellon(settings)) + + for statement in self.statements: + x0, y0 = statement.start + x1, y1 = statement.end + if isinstance(statement, DxfArcStatement): + 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=x1, y=y1, i=i, j=j).to_excellon(settings)) + else: + excellon += 'G01{0}\n'.format( + CoordinateStmtEx(x=x1, y=y1).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 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 .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 filter(lambda s: not isinstance(s, DxfPolylineStatement), statements): + for path in paths: + if path.contain(statement): + redundant += 1 + break + else: + for target in unique_statements: + if statement.is_equal_to(target, error_range): + redundant += 1 + break + else: + unique_statements.append(statement) + + paths.extend([DxfPath([s], error_range) for s in unique_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)): + target = paths[j] + if target.merge(mergee, error_range): + break + else: + working.append(mergee) + prev_paths_num = len(paths) + paths = working + + 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 .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/gerbonara/gerber/panelize/excellon.py b/gerbonara/gerber/panelize/excellon.py new file mode 100644 index 0000000..ae0b68e --- /dev/null +++ b/gerbonara/gerber/panelize/excellon.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +import operator + +from .. import excellon +from ..excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot +from ..excellon_statements import ExcellonStatement, UnitStmt, CoordinateStmt, UnknownStmt, \ + SlotStmt, DrillModeStmt, RouteModeStmt, LinearModeStmt, \ + ToolSelectionStmt, ZAxisRoutPositionStmt, \ + RetractWithClampingStmt, RetractWithoutClampingStmt, \ + EndOfProgramStmt +from ..cam import FileSettings +from ..utils import inch, metric, write_gerber_value, parse_gerber_value +from .utility import rotate + +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 + excellon.CoordinateStmt = CoordinateStmtEx + excellon.UnitStmt = UnitStmtEx + 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): + def correct_statements(): + for stmt in file.statements: + if isinstance(stmt, UnknownStmt): + line = stmt.stmt.strip() + if line[:3] == 'G02': + yield CircularCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) + elif line[:3] == 'G03': + 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 + 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_mode = None + coordinate_ctx = CoordinateCtx(file.notation) + rout_nodes = [] + + last_position = (0., 0.) + last_radius = None + last_center_offset = None + + def make_rout(status, nodes): + if status != STAT_ROUT_DOWN or len(nodes) == 0 or current_tool is None: + return None + return DrillRout(current_tool, nodes) + + for stmt in statements: + if isinstance(stmt, ToolSelectionStmt): + current_tool = file.tools[stmt.tool] + elif isinstance(stmt, DrillModeStmt): + rout = make_rout(status, rout_nodes) + rout_nodes = [] + 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_nodes) + rout_nodes = [] + if rout is not None: + yield rout + status = STAT_ROUT_UP + elif isinstance(stmt, SlotStmt): + 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): + 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_nodes.append(coordinate_ctx.node(rout_mode, center_offset)) + + 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) + + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + for hit in self.hits: + hit.rotate(angle, center) + + def to_inch(self): + if self.units == 'metric': + 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() + self.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + 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() + 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]) + 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 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) + + def to_excellon(self, settings): + 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=None, center_offset=None): + self.mode = mode + self.position = (x, y) + self.radius = radius + self.center_offset = center_offset + + 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): + excellon = self.nodes[0].to_excellon(settings) + '\nM15\n' + for node in self.nodes[1:]: + excellon += node.to_excellon(settings) + '\n' + excellon += 'M16\nG05' + return excellon + + def to_inch(self): + 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 + 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: + 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) + if node.center_offset is not None: + node.center_offset = rotate(*node.center_offset, angle, (0., 0.)) + +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): + format = settings.format if settings else self.format + 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 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.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None + return newStmt + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + 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, i=None, j=None, **kwargs): + super(CoordinateStmtEx, self).__init__(x, y, **kwargs) + self.radius = radius + self.i = i + self.j = j + + def to_excellon(self, settings): + stmt = '' + 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) + 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): + 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 + 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) diff --git a/gerbonara/gerber/panelize/gerber_statements.py b/gerbonara/gerber/panelize/gerber_statements.py new file mode 100644 index 0000000..875d656 --- /dev/null +++ b/gerbonara/gerber/panelize/gerber_statements.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..gerber_statements import AMParamStmt, ADParamStmt +from ..utils import inch, metric +from .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/gerbonara/gerber/panelize/rs274x.py b/gerbonara/gerber/panelize/rs274x.py new file mode 100644 index 0000000..2f44cd4 --- /dev/null +++ b/gerbonara/gerber/panelize/rs274x.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..cam import FileSettings +from .. import rs274x +from ..gerber_statements import * +from .gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx +from .utility import rotate +import re + +def loads(data, filename=None): + cls = 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(rs274x.GerberFile): + @classmethod + def from_gerber_file(cls, gerber_file): + if not isinstance(gerber_file, rs274x.GerberFile): + raise Exception('only gerbonara.gerber.rs274x.GerberFile object is specified') + + 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) + if self.context.is_negative: + self.nagate_polarity() + 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.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 + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) + + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + self._generalize_aperture() + last_x = 0 + last_y = 0 + last_rx = 0 + last_ry = 0 + 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 + 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 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 + 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 + for statement in self.aperture_defs: + if isinstance(statement, ADParamStmt) and statement.shape in ['R', 'O', 'P']: + need_to_change = True + + 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 self.aperture_macros: + name = '%s_%d' % (macro_def[0], num) + num += 1 + self.aperture_macros[name] = macro_def[1](name, self.units) + macro_defs[idx] = (name, macro_def[1]) + for statement in self.aperture_defs: + if isinstance(statement, ADParamStmt): + if statement.shape == 'R': + statement.shape = macro_defs[RECTANGLE][0] + elif statement.shape == 'O': + 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': + 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.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 + self.direction = self.DIR_CLOCKWISE + self.x = 0. + self.y = 0. + + def normalize_statement(self, stmt): + additional_stmts = None + 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, 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 + 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.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: + 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 + 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: + 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 diff --git a/gerbonara/gerber/panelize/utility.py b/gerbonara/gerber/panelize/utility.py new file mode 100644 index 0000000..37de5e8 --- /dev/null +++ b/gerbonara/gerber/panelize/utility.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from math import cos, sin, pi, sqrt + +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]) + +def is_equal_value(a, b, error_range=0): + 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 \ + 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