From 9febca7da6a730b3b3ca3a54129a9f88e5c44d14 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Thu, 21 Mar 2019 22:00:32 +0900
Subject: initial commit

---
 gerberex/__init__.py      |   8 +
 gerberex/am_expression.py | 183 +++++++++++++++++++
 gerberex/am_primitive.py  | 437 ++++++++++++++++++++++++++++++++++++++++++++++
 gerberex/common.py        |  35 ++++
 gerberex/composition.py   | 201 +++++++++++++++++++++
 gerberex/dxf.py           | 357 +++++++++++++++++++++++++++++++++++++
 gerberex/excellon.py      |  42 +++++
 gerberex/rs274x.py        |  25 +++
 gerberex/statements.py    |  34 ++++
 9 files changed, 1322 insertions(+)
 create mode 100644 gerberex/__init__.py
 create mode 100644 gerberex/am_expression.py
 create mode 100644 gerberex/am_primitive.py
 create mode 100644 gerberex/common.py
 create mode 100644 gerberex/composition.py
 create mode 100644 gerberex/dxf.py
 create mode 100644 gerberex/excellon.py
 create mode 100644 gerberex/rs274x.py
 create mode 100644 gerberex/statements.py

(limited to 'gerberex')

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


From 690df56bb71020901167605a87ec451081fa18d7 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Sat, 23 Mar 2019 21:59:13 +0900
Subject: add rotation fuction

---
 gerberex/am_primitive.py | 35 +++++++++++++++++---------
 gerberex/composition.py  |  2 +-
 gerberex/dxf.py          |  2 +-
 gerberex/excellon.py     |  7 ++++++
 gerberex/rs274x.py       | 65 +++++++++++++++++++++++++++++++++++++++++++++++-
 gerberex/statements.py   | 38 +++++++++++++++++++++++++++-
 gerberex/utility.py      | 13 ++++++++++
 7 files changed, 146 insertions(+), 16 deletions(-)
 create mode 100644 gerberex/utility.py

(limited to 'gerberex')

diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py
index 3ce047a..82370f6 100644
--- a/gerberex/am_primitive.py
+++ b/gerberex/am_primitive.py
@@ -7,13 +7,21 @@ from gerber.utils import *
 from gerber.am_statements import *
 from gerber.am_eval import OpCode
 
-from gerberex.am_expression import eval_macro
+from gerberex.am_expression import eval_macro, AMConstantExpression, AMOperatorExpression
 
 class AMPrimitiveDef(AMPrimitive):
-    def __init__(self, code, exposure=None, rotation=0):
+    def __init__(self, code, exposure=None, rotation=None):
         super(AMPrimitiveDef, self).__init__(code, exposure)
+        if not rotation:
+            rotation = AMConstantExpression(0)
         self.rotation = rotation
 
+    def rotate(self, angle, center=None):
+        self.rotation = AMOperatorExpression(AMOperatorExpression.ADD, 
+                                             self.rotation, 
+                                             AMConstantExpression(float(angle)))
+        self.rotation = self.rotation.optimize()
+
     def to_inch(self):
         pass
     
@@ -44,12 +52,12 @@ class AMCommentPrimitiveDef(AMPrimitiveDef):
 class AMCirclePrimitiveDef(AMPrimitiveDef):
     @classmethod
     def from_modifiers(cls, code, modifiers):
-        exposure = 'on' if modifiers[0] == 1 else 'off',
-        diameter = modifiers[1],
-        center_x = modifiers[2],
-        center_y = modifiers[3],
+        exposure = 'on' if modifiers[0].value == 1 else 'off'
+        diameter = modifiers[1]
+        center_x = modifiers[2]
+        center_y = modifiers[3]
         rotation = modifiers[4]
-        return cls(code, expressions, center_x, center_y, rotation)
+        return cls(code, exposure, diameter, center_x, center_y, rotation)
 
     def __init__(self, code, exposure, diameter, center_x, center_y, rotation):
         super(AMCirclePrimitiveDef, self).__init__(code, exposure, rotation)
@@ -87,7 +95,7 @@ class AMVectorLinePrimitiveDef(AMPrimitiveDef):
     @classmethod
     def from_modifiers(cls, code, modifiers):
         code = code
-        exposure = 'on' if modifiers[0] == 1 else 'off'
+        exposure = 'on' if modifiers[0].value == 1 else 'off'
         width = modifiers[1]
         start_x = modifiers[2]
         start_y = modifiers[3]
@@ -141,7 +149,7 @@ class AMCenterLinePrimitiveDef(AMPrimitiveDef):
     @classmethod
     def from_modifiers(cls, code, modifiers):
         code = code
-        exposure = 'on' if modifiers[0] == 1 else 'off'
+        exposure = 'on' if modifiers[0].value == 1 else 'off'
         width = modifiers[1]
         height = modifiers[2]
         x = modifiers[3]
@@ -191,7 +199,7 @@ class AMOutlinePrimitiveDef(AMPrimitiveDef):
     def from_modifiers(cls, code, modifiers):
         num_points = modifiers[1] + 1
         code = code
-        exposure = 'on' if modifiers[0] == 1 else 'off'
+        exposure = 'on' if modifiers[0].value == 1 else 'off'
         addrs = modifiers[2:num_points * 2]
         rotation = modifiers[3 + num_points * 2]
         return cls(code, exposure, addrs, rotation)
@@ -231,7 +239,7 @@ class AMPolygonPrimitiveDef(AMPrimitiveDef):
     @classmethod
     def from_modifiers(cls, code, modifiers):
         code = code
-        exposure = 'on' if modifiers[0] == 1 else 'off'
+        exposure = 'on' if modifiers[0].value == 1 else 'off'
         vertices = modifiers[1]
         x = modifiers[2]
         y = modifiers[3]
@@ -417,6 +425,9 @@ class AMVariableDef(object):
             yield i
         yield (OpCode.STORE, self.number)
 
+    def rotate(self, angle, center=None):
+        pass
+
 def to_primitive_defs(instructions):
     classes = {
         0: AMCommentPrimitiveDef,
@@ -434,4 +445,4 @@ def to_primitive_defs(instructions):
             yield AMVariableDef(-code, modifiers[0])
         else:
             primitive = classes[code]
-            yield primitive.from_modifiers(code, modifiers)
\ No newline at end of file
+            yield primitive.from_modifiers(code, modifiers)
diff --git a/gerberex/composition.py b/gerberex/composition.py
index 2613a61..afcaf97 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -101,7 +101,7 @@ class GerberComposition(Composition):
 
         if not self.settings:
             self.settings = file.settings
-            self.param_statements = file.header
+            self.param_statements = [file.header]
 
 
     def _register_aperture_macro(self, statement):
diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index b641924..11072b5 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -111,7 +111,7 @@ class DxfArcStatement(DxfStatement):
                                settings.zero_suppression),
             write_gerber_value(begin_y, settings.format,
                                settings.zero_suppression),
-            '03' if deg0 > deg1 else '02',
+            '03',
             write_gerber_value(end_x, settings.format,
                                settings.zero_suppression),
             write_gerber_value(end_y, settings.format,
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 78e6e5f..90d6742 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -6,6 +6,7 @@
 from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile)
 from gerber.excellon_statements import UnitStmt
 from gerber.cam import FileSettings
+from gerberex.utility import rotate
 
 def loads(data, filename=None, settings=None, tools=None, format=None):
     if not settings:
@@ -27,6 +28,12 @@ class ExcellonFileEx(ExcellonFile):
     def __init__(self, statements, tools, hits, settings, filename=None):
         super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename)
 
+    def rotate(self, angle, center=(0,0)):
+        if angle % 360 == 0:
+            return
+        for hit in self.hits:
+            hit.position = rotate(hit.position[0], hit.position[1], angle, center)
+
 class UnitStmtEx(UnitStmt):
     @classmethod
     def from_statement(cls, stmt):
diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
index e9d82cd..4b477d3 100644
--- a/gerberex/rs274x.py
+++ b/gerberex/rs274x.py
@@ -4,7 +4,9 @@
 # Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
 
 import gerber.rs274x
-from gerberex.statements import (AMParamStmt, AMParamStmtEx)
+from gerber.gerber_statements import ADParamStmt, CoordStmt
+from gerberex.statements import AMParamStmt, AMParamStmtEx
+from gerberex.utility import rotate
 
 class GerberFile(gerber.rs274x.GerberFile):
     @classmethod
@@ -23,3 +25,64 @@ class GerberFile(gerber.rs274x.GerberFile):
 
     def __init__(self, statements, settings, primitives, apertures, filename=None):
         super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename)
+
+    def rotate(self, angle, center=(0,0)):
+        if angle % 360 == 0:
+            return
+        self._generalize_aperture()
+        for statement in self.statements:
+            if isinstance(statement, AMParamStmtEx):
+                statement.rotate(angle, center)
+            elif isinstance(statement, CoordStmt) and statement.x != None and statement.y != None:
+                statement.x, statement.y = rotate(statement.x, statement.y, angle, center)
+    
+    def _generalize_aperture(self):
+        RECTANGLE = 0
+        LANDSCAPE_OBROUND = 1
+        PORTRATE_OBROUND = 2
+        POLYGON = 3
+        macro_defs = [
+            ('MACR', AMParamStmtEx.rectangle),
+            ('MACLO', AMParamStmtEx.landscape_obround),
+            ('MACPO', AMParamStmtEx.portrate_obround),
+            ('MACP', AMParamStmtEx.polygon)
+        ]
+
+        need_to_change = False
+        insert_point = 0
+        last_aperture = 0
+        macros = {}
+        for idx in range(0, len(self.statements)):
+            statement = self.statements[idx]
+            if isinstance(statement, AMParamStmtEx):
+                macros[statement.name] = statement
+                if not need_to_change:
+                    insert_point = idx + 1
+            if isinstance(statement, ADParamStmt) and statement.shape in ['R', 'O', 'P']:
+                need_to_change = True
+                last_aperture = idx
+        
+        if need_to_change:
+            for idx in range(0, len(macro_defs)):
+                macro_def = macro_defs[idx]
+                name = macro_def[0]
+                num = 1
+                while name in macros:
+                    name = '%s_%d' % (macro_def[0], num)
+                    num += 1
+                self.statements.insert(insert_point, macro_def[1](name))
+                macro_defs[idx] = (name, macro_def[1])
+            for idx in range(insert_point, last_aperture + len(macro_defs) + 1):
+                statement = self.statements[idx]
+                if isinstance(statement, ADParamStmt):
+                    if statement.shape == 'R':
+                        statement.shape = macro_defs[RECTANGLE][0]
+                    elif statement.shape == 'O':
+                        x = statement.modifiers[0] \
+                            if len(statement.modifiers) > 0 else 0
+                        y = statement.modifiers[1] \
+                            if len(statement.modifiers) > 1 else 0
+                        statement.shape = macro_defs[LANDSCAPE_OBROUND][0] \
+                                          if x > y else macro_defs[PORTRATE_OBROUND][0] 
+                    elif statement.shape == 'P':
+                        statement.shape = macro_defs[POLYGON][0]
diff --git a/gerberex/statements.py b/gerberex/statements.py
index 77ca235..c41acb9 100644
--- a/gerberex/statements.py
+++ b/gerberex/statements.py
@@ -11,9 +11,41 @@ class AMParamStmtEx(AMParamStmt):
     def from_stmt(cls, stmt):
         return cls(stmt.param, stmt.name, stmt.macro)
 
+    @classmethod
+    def circle(cls, name):
+        return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0')
+
+    @classmethod
+    def rectangle(cls, name):
+        return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0')
+    
+    @classmethod
+    def landscape_obround(cls, name):
+        return cls(
+            'AM', name,
+            '$4=$1-$2*'
+            '21,1,$1-$4,$2,0,0,0*'
+            '1,1,$4,$4/2,0,0*'
+            '1,1,$4,-$4/2,0,0*'
+            '1,0,$3,0,0,0')
+
+    @classmethod
+    def portrate_obround(cls, name):
+        return cls(
+            'AM', name,
+            '$4=$2-$1*'
+            '21,1,$1,$2-$4,0,0,0*'
+            '1,1,$4,0,$4/2,0*'
+            '1,1,$4,0,-$4/2,0*'
+            '1,0,$3,0,0,0')
+    
+    @classmethod
+    def polygon(cls, name):
+        return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0')
+
     def __init__(self, param, name, macro):
         super(AMParamStmtEx, self).__init__(param, name, macro)
-        self.primitive_defs = to_primitive_defs(self.instructions)
+        self.primitive_defs = list(to_primitive_defs(self.instructions))
     
     def to_inch(self):
         if self.units == 'metric':
@@ -32,3 +64,7 @@ class AMParamStmtEx(AMParamStmt):
             for p in self.primitive_defs:
                 yield p.to_gerber(settings)
         return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))
+
+    def rotate(self, angle, center=None):
+        for primitive_def in self.primitive_defs:
+            primitive_def.rotate(angle, center)
diff --git a/gerberex/utility.py b/gerberex/utility.py
new file mode 100644
index 0000000..852519a
--- /dev/null
+++ b/gerberex/utility.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+from math import cos, sin, pi
+
+def rotate(x, y, angle, center):
+    x0 = x - center[0]
+    y0 = y - center[1]
+    angle = angle * pi / 180.0
+    return (cos(angle) * x0 - sin(angle) * y0 + center[0],
+            sin(angle) * x0 + cos(angle) * y0 + center[1])
-- 
cgit 


From 7e8f90b3724c62f72cf58ad28c11506212f3b706 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Mon, 25 Mar 2019 20:21:41 +0900
Subject: fix a setup issue

---
 gerberex/__init__.py | 7 +++++++
 1 file changed, 7 insertions(+)

(limited to 'gerberex')

diff --git a/gerberex/__init__.py b/gerberex/__init__.py
index 388f592..cef11ab 100644
--- a/gerberex/__init__.py
+++ b/gerberex/__init__.py
@@ -2,6 +2,13 @@
 # -*- coding: utf-8 -*-
 
 # Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+"""
+Gerber Tools Extension
+======================
+**Gerber Tools Extenstion**
+gerber-tools-extension is a extention package for gerber-tools.
+This package provide panelizing of PCB fucntion.
+"""
 
 from gerberex.common import (read, loads)
 from gerberex.composition import (GerberComposition, DrillComposition)
-- 
cgit 


From fcd704e1eef9034e2000f55b2918d7df41379408 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Sat, 30 Mar 2019 11:16:13 +0900
Subject: add mouse bites generator function

---
 gerberex/composition.py |  22 ++++++
 gerberex/dxf.py         | 205 ++++++++++++++++++++++++++++++++++++++----------
 2 files changed, 184 insertions(+), 43 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/composition.py b/gerberex/composition.py
index afcaf97..73f5702 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -127,10 +127,13 @@ class DrillComposition(Composition):
         self.header2_statements = []
         self.tools = []
         self.hits = []
+        self.dxf_statements = []
     
     def merge(self, file):
         if isinstance(file, gerberex.excellon.ExcellonFileEx):
             self._merge_excellon(file)
+        elif isinstance(file, gerberex.DxfFile):
+            self._merge_dxf(file)
         else:
             raise Exception('unsupported file type')
 
@@ -147,6 +150,9 @@ class DrillComposition(Composition):
                 for h in self.hits:
                     if h.tool.number == t.number:
                         yield CoordinateStmt(*h.position).to_excellon(self.settings)
+                for num, statement in self.dxf_statements:
+                    if num == t.number:
+                        yield statement.to_excellon(self.settings)
             yield EndOfProgramStmt().to_excellon()
         
         with open(path, 'w') as f:
@@ -186,6 +192,22 @@ class DrillComposition(Composition):
             hit.tool = tool_map[hit.tool.number]
             self.hits.append(hit)
     
+    def _merge_dxf(self, file):
+        if not self.settings:
+            self.settings = file.settings
+        else:
+            if self.settings.units == 'metric':
+                file.to_metric()
+            else:
+                file.to_inch()
+
+        if not self.header1_statements:
+            self.header1_statements = file.header
+            self.header2_statements = file.header2
+        
+        tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width))
+        self.dxf_statements.append((tool.number, file.statements))
+
     def _register_tool(self, tool):
         for existing in self.tools:
             if existing.equivalent(tool):
diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 11072b5..a2c26d0 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -8,13 +8,18 @@ from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt
 from gerber.cam import CamFile, FileSettings
 from gerber.utils import inch, metric, write_gerber_value
 from gerber.gerber_statements import ADParamStmt
+from gerber.excellon_statements import ExcellonTool
+from gerber.excellon_statements import CoordinateStmt
 import dxfgrabber
 
 class DxfStatement(object):
     def __init__(self, entity):
         self.entity = entity
 
-    def to_gerber(self, settings=None):
+    def to_gerber(self, settings=None, pitch=0, width=0):
+        pass
+
+    def to_excellon(self, settings=None, pitch=0, width=0):
         pass
 
     def to_inch(self):
@@ -27,39 +32,77 @@ class DxfLineStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfLineStatement, self).__init__(entity)
     
-    def to_gerber(self, settings=FileSettings):
-        x0 = self.entity.start[0]
-        y0 = self.entity.start[1]
-        x1 = self.entity.end[0]
-        y1 = self.entity.end[1]
-        return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format(
-            write_gerber_value(x0, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(y0, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(x1, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(y1, settings.format,
-                               settings.zero_suppression)
-        )
+    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
+        if pitch == 0:
+            x0 = self.entity.start[0]
+            y0 = self.entity.start[1]
+            x1 = self.entity.end[0]
+            y1 = self.entity.end[1]
+            return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format(
+                write_gerber_value(x0, settings.format,
+                                   settings.zero_suppression),
+                write_gerber_value(y0, settings.format,
+                                   settings.zero_suppression),
+                write_gerber_value(x1, settings.format,
+                                   settings.zero_suppression),
+                write_gerber_value(y1, settings.format,
+                                   settings.zero_suppression)
+            )
+        else:
+            gstr = ""
+            for p in self._dots(pitch, width):
+                gstr += 'X{0}Y{1}D03*\n'.format(
+                    write_gerber_value(p[0], settings.format,
+                                       settings.zero_suppression),
+                    write_gerber_value(p[1], settings.format, 
+                                       settings.zero_suppression))
+            return gstr
+
+    def to_excellon(self, settings=FileSettings(), pitch=0, width=0):
+        if not pitch:
+            return
+        gstr = ""
+        for p in self._dots(pitch, width):
+            gstr += CoordinateStmt(x=p[0], y=p[1]).to_excellon(settings) + '\n'
+        return gstr
 
     def to_inch(self):
-        self.entity.start[idx] = (
-            inch(self.entity.start[idx][0]), inch(self.entity.start[idx][1]))
-        self.entity.end[idx] = (
-            inch(self.entity.end[idx][0]), inch(self.entity.end[idx][1]))
+        self.entity.start = (
+            inch(self.entity.start[0]), inch(self.entity.start[1]))
+        self.entity.end = (
+            inch(self.entity.end[0]), inch(self.entity.end[1]))
 
     def to_metric(self):
-        self.entity.start[idx] = (
-            metric(self.entity.start[idx][0]), inch(self.entity.start[idx][1]))
-        self.entity.end[idx] = (
-            metric(self.entity.end[idx][0]), inch(self.entity.end[idx][1]))
+        self.entity.start = (
+            metric(self.entity.start[0]), inch(self.entity.start[1]))
+        self.entity.end = (
+            metric(self.entity.end[0]), inch(self.entity.end[1]))
+
+    def _dots(self, pitch, width):
+        x0 = self.entity.start[0]
+        y0 = self.entity.start[1]
+        x1 = self.entity.end[0]
+        y1 = self.entity.end[1]
+        xp = x1 - x0
+        yp = y1 - y0
+        l = sqrt(xp * xp + yp * yp)
+        xd = xp * pitch / l
+        yd = yp * pitch / l
+
+        d = 0;
+        while d < l + width / 2:
+            yield (x0, y0)
+            x0 += xd
+            y0 += yd
+            d += pitch
 
 class DxfCircleStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfCircleStatement, self).__init__(entity)
 
-    def to_gerber(self, settings=FileSettings):
+    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
+        if pitch:
+            return
         r = self.entity.radius
         x0 = self.entity.center[0]
         y0 = self.entity.center[1]
@@ -82,19 +125,21 @@ class DxfCircleStatement(DxfStatement):
 
     def to_inch(self):
         self.entity.radius = inch(self.entity.radius)
-        self.entity.center[idx] = (
-            inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1]))
+        self.entity.center = (
+            inch(self.entity.center[0]), inch(self.entity.center[1]))
 
     def to_metric(self):
         self.entity.radius = metric(self.entity.radius)
-        self.entity.center[idx] = (
-            metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1]))
+        self.entity.center = (
+            metric(self.entity.center[0]), metric(self.entity.center[1]))
 
 class DxfArcStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfArcStatement, self).__init__(entity)
 
-    def to_gerber(self, settings=FileSettings):
+    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
+        if pitch:
+            return
         deg0 = self.entity.start_angle
         deg1 = self.entity.end_angle
         r = self.entity.radius
@@ -126,21 +171,23 @@ class DxfArcStatement(DxfStatement):
         self.entity.start_angle = inch(self.entity.start_angle)
         self.entity.end_angle = inch(self.entity.end_angle)
         self.entity.radius = inch(self.entity.radius)
-        self.entity.center[idx] = (
-            inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1]))
+        self.entity.center = (
+            inch(self.entity.center[0]), inch(self.entity.center[1]))
 
     def to_metric(self):
         self.entity.start_angle = metric(self.entity.start_angle)
         self.entity.end_angle = metric(self.entity.end_angle)
         self.entity.radius = metric(self.entity.radius)
-        self.entity.center[idx] = (
-            metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1]))
+        self.entity.center = (
+            metric(self.entity.center[0]), metric(self.entity.center[1]))
 
 class DxfPolylineStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfPolylineStatement, self).__init__(entity)
 
-    def to_gerber(self, settings=FileSettings()):
+    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
+        if pitch:
+            return
         x0 = self.entity.points[0][0]
         y0 = self.entity.points[0][1]
         b = self.entity.bulge[0]
@@ -214,6 +261,8 @@ class DxfStatements(object):
         self._units = units
         self.dcode = dcode
         self.draw_mode = draw_mode
+        self.pitch = inch(1) if self._units == 'unit' else 1
+        self.width = 0
         self.statements = []
         for entity in entities:
             if entity.dxftype == 'LWPOLYLINE':
@@ -241,19 +290,33 @@ class DxfStatements(object):
                 yield 'G37*'
             else:
                 for statement in self.statements:
-                    yield statement.to_gerber(settings)
+                    yield statement.to_gerber(
+                        settings, 
+                        pitch=self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0,
+                        width=self.width)
 
         return '\n'.join(gerbers())
 
+    def to_excellon(self, settings=FileSettings()):
+        if not self.draw_mode == DxfFile.DM_MOUSE_BITES:
+            return
+        def drills():
+            for statement in self.statements:
+                if isinstance(statement, DxfLineStatement):
+                    yield statement.to_excellon(settings, pitch=self.pitch, width=self.width)
+        return '\n'.join(drills())
+
     def to_inch(self):
         if self._units == 'metric':
             self._units = 'inch'
+            self.pitch = inch(self.pitch)
             for statement in self.statements:
                 statement.to_inch()
 
     def to_metric(self):
         if self._units == 'inch':
             self._units = 'metric'
+            self.pitch = metric(self.pitch)
             for statement in self.statements:
                 statement.to_metric()
 
@@ -270,6 +333,32 @@ class DxfHeaderStatement(object):
             settings.format[0], settings.format[1],
             settings.format[0], settings.format[1]
         )
+    
+    def to_excellon(self, settings):
+        return 'M48\n'\
+               'FMAT,2\n'\
+               'ICI,{0}\n'\
+               '{1},{2},{3}.{4}\n'\
+               '{5}'.format(
+            'ON' if settings.notation == 'incremental' else 'OFF',
+            'INCH' if settings.units == 'inch' else 'METRIC',
+            'TZ' if settings.zero_suppression == 'leading' else 'LZ',
+            '0' * settings.format[0], '0' * settings.format[1],
+            'M72' if settings.units == 'inch' else 'M71'
+        )
+
+    def to_inch(self):
+        pass
+
+    def to_metric(self):
+        pass
+
+class DxfHeader2Statement(object):
+    def to_gerber(self, settings):
+        pass
+
+    def to_excellon(self, settings):
+        return '%'
 
     def to_inch(self):
         pass
@@ -280,8 +369,15 @@ class DxfHeaderStatement(object):
 class DxfFile(CamFile):
     DM_LINE = 0
     DM_FILL = 1
+    DM_MOUSE_BITES = 2
+
+    FT_RX274X = 0
+    FT_EXCELLON = 1
+
+    def __init__(self, dxf, settings=None, draw_mode=None, filename=None):
+        if not settings:
+            settings = FileSettings(zero_suppression='leading')
 
-    def __init__(self, dxf, settings=FileSettings(), draw_mode=None, filename=None):
         if draw_mode == None:
             draw_mode = self.DM_LINE
         if dxf.header['$INSUNITS'] == 1:
@@ -294,6 +390,8 @@ class DxfFile(CamFile):
         super(DxfFile, self).__init__(settings=settings, filename=filename)
         self._draw_mode = draw_mode
         self.header = DxfHeaderStatement()
+        
+        self.header2 = DxfHeader2Statement()
         self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
         self.statements = DxfStatements(dxf.entities, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
 
@@ -313,6 +411,7 @@ class DxfFile(CamFile):
     @width.setter
     def width(self, value):
         self.aperture.modifiers = ([float(value),],)
+        self.statements.width = value
 
     @property
     def draw_mode(self):
@@ -322,23 +421,42 @@ class DxfFile(CamFile):
     def draw_mode(self, value):
         self._draw_mode = value
         self.statements.draw_mode = value
+
+    @property
+    def pitch(self):
+        return self.statements.pitch
     
-    def write(self, filename=None):
+    @pitch.setter
+    def pitch(self, value):
+        self.statements.pitch = value
+    
+    def write(self, filename=None, filetype=FT_RX274X):
         if self.settings.notation != 'absolute':
             raise Exception('DXF file\'s notation must be absolute ')
-
+        
         filename = filename if filename is not None else self.filename
         with open(filename, 'w') as f:
-            f.write(self.header.to_gerber(self.settings) + '\n')
-            f.write(self.aperture.to_gerber(self.settings) + '\n')
-            f.write(self.statements.to_gerber(self.settings) + '\n')
-            f.write('M02*\n')
+            if filetype == self.FT_RX274X:
+                f.write(self.header.to_gerber(self.settings) + '\n')
+                f.write(self.aperture.to_gerber(self.settings) + '\n')
+                f.write(self.statements.to_gerber(self.settings) + '\n')
+                f.write('M02*\n')
+            else:
+                tool = ExcellonTool(self.settings, number=1, diameter=self.width)
+                f.write(self.header.to_excellon(self.settings) + '\n')
+                f.write(tool.to_excellon(self.settings) + '\n')
+                f.write(self.header2.to_excellon(self.settings) + '\n')
+                f.write('T01\n')
+                f.write(self.statements.to_excellon(self.settings) + '\n')
+                f.write('M30\n')
+
 
     def to_inch(self):
         if self.units == 'metric':
             self.header.to_inch()
             self.aperture.to_inch()
             self.statements.to_inch()
+            self.pitch = inch(self.pitch)
             self.units = 'inch'
 
     def to_metric(self):
@@ -346,6 +464,7 @@ class DxfFile(CamFile):
             self.header.to_metric()
             self.aperture.to_metric()
             self.statements.to_metric()
+            self.pitch = metric(self.pitch)
             self.units = 'metric'
 
     def offset(self, ofset_x, offset_y):
-- 
cgit 


From 900d992fa3af05f93ac7a4cf717f28598e1a868d Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Sun, 31 Mar 2019 13:30:15 +0900
Subject: auto detection closed paths in the collection of DXF arc object and
 line object, then fill these closed path

---
 gerberex/dxf.py      | 198 +++++++++++++++++++++++++++++++++++++--------------
 gerberex/dxf_path.py | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 342 insertions(+), 53 deletions(-)
 create mode 100644 gerberex/dxf_path.py

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index a2c26d0..e6eb971 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -5,16 +5,29 @@
 
 import io
 from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt
+import dxfgrabber
 from gerber.cam import CamFile, FileSettings
 from gerber.utils import inch, metric, write_gerber_value
 from gerber.gerber_statements import ADParamStmt
 from gerber.excellon_statements import ExcellonTool
 from gerber.excellon_statements import CoordinateStmt
-import dxfgrabber
+from gerberex.dxf_path import generate_closed_paths
+
+ACCEPTABLE_ERROR = 0.001
+
+def is_equal_value(a, b, error_range=0):
+    return a - b <= error_range and a - b >= -error_range
+
+def is_equal_point(a, b, error_range=0):
+    return is_equal_value(a[0], b[0], error_range) and \
+           is_equal_value(a[1], b[1], error_range)
 
 class DxfStatement(object):
     def __init__(self, entity):
         self.entity = entity
+        self.start = None
+        self.end = None
+        self.is_closed = False
 
     def to_gerber(self, settings=None, pitch=0, width=0):
         pass
@@ -28,16 +41,22 @@ class DxfStatement(object):
     def to_metric(self):
         pass
 
+    def is_equal_to(self, target, error_range=0):
+        return False
+
+    def reverse(self):
+        raise Exception('Not implemented')
+
 class DxfLineStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfLineStatement, self).__init__(entity)
+        self.start = (self.entity.start[0], self.entity.start[1])
+        self.end = (self.entity.end[0], self.entity.end[1])
     
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         if pitch == 0:
-            x0 = self.entity.start[0]
-            y0 = self.entity.start[1]
-            x1 = self.entity.end[0]
-            y1 = self.entity.end[1]
+            x0, y0 = self.start
+            x1, y1 = self.end
             return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format(
                 write_gerber_value(x0, settings.format,
                                    settings.zero_suppression),
@@ -67,22 +86,34 @@ class DxfLineStatement(DxfStatement):
         return gstr
 
     def to_inch(self):
-        self.entity.start = (
-            inch(self.entity.start[0]), inch(self.entity.start[1]))
-        self.entity.end = (
-            inch(self.entity.end[0]), inch(self.entity.end[1]))
+        self.start = (
+            inch(self.start[0]), inch(self.start[1]))
+        self.end = (
+            inch(self.end[0]), inch(self.end[1]))
 
     def to_metric(self):
-        self.entity.start = (
-            metric(self.entity.start[0]), inch(self.entity.start[1]))
-        self.entity.end = (
-            metric(self.entity.end[0]), inch(self.entity.end[1]))
+        self.start = (
+            metric(self.start[0]), inch(self.start[1]))
+        self.end = (
+            metric(self.end[0]), inch(self.end[1]))
+
+    def is_equal_to(self, target, error_range=0):
+        if not isinstance(target, DxfLineStatement):
+            return False
+        return (is_equal_point(self.start, target.start, error_range) and \
+                is_equal_point(self.end, target.end, error_range)) or \
+               (is_equal_point(self.start, target.end, error_range) and \
+                is_equal_point(self.end, target.start, error_range))
+
+    def reverse(self):
+        pt = self.start
+        self.start = self.end
+        self.end = pt
 
     def _dots(self, pitch, width):
-        x0 = self.entity.start[0]
-        y0 = self.entity.start[1]
-        x1 = self.entity.end[0]
-        y1 = self.entity.end[1]
+        x0, y0 = self.start
+        x1, y1 = self.end
+        y1 = self.end[1]
         xp = x1 - x0
         yp = y1 - y0
         l = sqrt(xp * xp + yp * yp)
@@ -99,13 +130,17 @@ class DxfLineStatement(DxfStatement):
 class DxfCircleStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfCircleStatement, self).__init__(entity)
+        self.radius = self.entity.radius
+        self.center = (self.entity.center[0], self.entity.center[1])
+        self.start = (self.center[0] + self.radius, self.center[1])
+        self.end = self.start
+        self.is_closed = True
 
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         if pitch:
             return
-        r = self.entity.radius
-        x0 = self.entity.center[0]
-        y0 = self.entity.center[1]
+        r = self.radius
+        x0, y0 = self.center
         return 'G01*\nX{0}Y{1}D02*\n' \
                'G75*\nG03*\nX{2}Y{3}I{4}J{5}D01*'.format(
             write_gerber_value(x0 + r, settings.format,
@@ -124,66 +159,107 @@ class DxfCircleStatement(DxfStatement):
         )
 
     def to_inch(self):
-        self.entity.radius = inch(self.entity.radius)
-        self.entity.center = (
-            inch(self.entity.center[0]), inch(self.entity.center[1]))
+        self.radius = inch(self.radius)
+        self.center = (
+            inch(self.center[0]), inch(self.center[1]))
 
     def to_metric(self):
-        self.entity.radius = metric(self.entity.radius)
-        self.entity.center = (
-            metric(self.entity.center[0]), metric(self.entity.center[1]))
+        self.radius = metric(self.radius)
+        self.center = (
+            metric(self.center[0]), metric(self.center[1]))
+
+    def is_equal_to(self, target, error_range=0):
+        if not isinstance(target, DxfCircleStatement):
+            return False
+        return is_equal_point(self.center, target.enter, error_range) and \
+               is_equal_value(self.radius, target.radius)
+
+    def reverse(self):
+        pass
 
 class DxfArcStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfArcStatement, self).__init__(entity)
+        self.start_angle = self.entity.start_angle
+        self.end_angle = self.entity.end_angle
+        self.radius = self.entity.radius
+        self.center = (self.entity.center[0], self.entity.center[1])
+        self.start = (
+            self.center[0] + self.radius * cos(self.start_angle / 180. * pi),
+            self.center[1] + self.radius * sin(self.start_angle / 180. * pi),
+        )
+        self.end = (
+            self.center[0] + self.radius * cos(self.end_angle / 180. * pi),
+            self.center[1] + self.radius * sin(self.end_angle / 180. * pi),
+        )
+        angle = self.end_angle - self.start_angle
+        self.is_closed = angle >= 360 or angle <= -360
 
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         if pitch:
             return
-        deg0 = self.entity.start_angle
-        deg1 = self.entity.end_angle
-        r = self.entity.radius
-        x0 = self.entity.center[0]
-        y0 = self.entity.center[1]
-        begin_x = x0 + r * cos(deg0 / 180. * pi)
-        begin_y = y0 + r * sin(deg0 / 180. * pi)
-        end_x = x0 + r * cos(deg1 / 180. * pi)
-        end_y = y0 + r * sin(deg1 / 180. * pi)
+        x0 = self.center[0]
+        y0 = self.center[1]
+        start_x, start_y = self.start
+        end_x, end_y = self.end
 
         return 'G01*\nX{0}Y{1}D02*\n' \
                'G75*\nG{2}*\nX{3}Y{4}I{5}J{6}D01*'.format(
-            write_gerber_value(begin_x, settings.format,
+            write_gerber_value(start_x, settings.format,
                                settings.zero_suppression),
-            write_gerber_value(begin_y, settings.format,
+            write_gerber_value(start_y, settings.format,
                                settings.zero_suppression),
-            '03',
+            '02' if self.start_angle > self.end_angle else '03',
             write_gerber_value(end_x, settings.format,
                                settings.zero_suppression),
             write_gerber_value(end_y, settings.format,
                                settings.zero_suppression),
-            write_gerber_value(x0 - begin_x, settings.format,
+            write_gerber_value(x0 - start_x, settings.format,
                                settings.zero_suppression),
-            write_gerber_value(y0 - begin_y, settings.format,
+            write_gerber_value(y0 - start_y, settings.format,
                                settings.zero_suppression)
         )
 
     def to_inch(self):
-        self.entity.start_angle = inch(self.entity.start_angle)
-        self.entity.end_angle = inch(self.entity.end_angle)
-        self.entity.radius = inch(self.entity.radius)
-        self.entity.center = (
-            inch(self.entity.center[0]), inch(self.entity.center[1]))
+        self.radius = inch(self.radius)
+        self.center = (inch(self.center[0]), inch(self.center[1]))
+        self.start = (inch(self.start[0]), inch(self.start[1]))
+        self.end = (inch(self.end[0]), inch(self.end[1]))
 
     def to_metric(self):
-        self.entity.start_angle = metric(self.entity.start_angle)
-        self.entity.end_angle = metric(self.entity.end_angle)
-        self.entity.radius = metric(self.entity.radius)
-        self.entity.center = (
-            metric(self.entity.center[0]), metric(self.entity.center[1]))
+        self.radius = metric(self.radius)
+        self.center = (metric(self.center[0]), metric(self.center[1]))
+        self.start = (metric(self.start[0]), metric(self.start[1]))
+        self.end = (metric(self.end[0]), metric(self.end[1]))
+
+    def is_equal_to(self, target, error_range=0):
+        if not isinstance(target, DxfArcStatement):
+            return False
+        aerror_range = error_range / pi * self.radius * 180
+        return is_equal_point(self.center, target.center, error_range) and \
+               is_equal_value(self.radius, target.radius, error_range) and \
+               ((is_equal_value(self.start_angle, target.start_angle, aerror_range) and 
+                 is_equal_value(self.end_angle, target.end_angle, aerror_range)) or
+                (is_equal_value(self.start_angle, target.end_angle, aerror_range) and
+                 is_equal_value(self.end_angle, target.end_angle, aerror_range)))
+
+    def reverse(self):
+        tmp = self.start_angle
+        self.start_angle = self.end_angle
+        self.end_angle = tmp
+        tmp = self.start
+        self.start = self.end
+        self.end = tmp
 
 class DxfPolylineStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfPolylineStatement, self).__init__(entity)
+        self.start = (self.entity.points[0][0], self.entity.points[0][1])
+        self.is_closed = self.entity.is_closed
+        if self.is_closed:
+            self.end = self.start
+        else:
+            self.start = (self.entity.points[-1][0], self.entity.points[-1][1])
 
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         if pitch:
@@ -243,12 +319,16 @@ class DxfPolylineStatement(DxfStatement):
         return gerber
 
     def to_inch(self):
+        self.start = (inch(self.start[0]), inch(self.start[1]))
+        self.end = (inch(self.end[0]), inch(self.end[1]))
         for idx in range(0, len(self.entity.points)):
             self.entity.points[idx] = (
                 inch(self.entity.points[idx][0]), inch(self.entity.points[idx][1]))
             self.entity.bulge[idx] = inch(self.entity.bulge[idx])
 
     def to_metric(self):
+        self.start = (metric(self.start[0]), metric(self.start[1]))
+        self.end = (metric(self.end[0]), metric(self.end[1]))
         for idx in range(0, len(self.entity.points)):
             self.entity.points[idx] = (
                 metric(self.entity.points[idx][0]), metric(self.entity.points[idx][1]))
@@ -261,8 +341,9 @@ class DxfStatements(object):
         self._units = units
         self.dcode = dcode
         self.draw_mode = draw_mode
-        self.pitch = inch(1) if self._units == 'unit' else 1
+        self.pitch = inch(1) if self._units == 'inch' else 1
         self.width = 0
+        self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR
         self.statements = []
         for entity in entities:
             if entity.dxftype == 'LWPOLYLINE':
@@ -273,6 +354,7 @@ class DxfStatements(object):
                 self.statements.append(DxfCircleStatement(entity))
             elif entity.dxftype == 'ARC':
                 self.statements.append(DxfArcStatement(entity))
+        self.paths = generate_closed_paths(self.statements, self.error_range)
 
     @property
     def units(self):
@@ -282,12 +364,16 @@ class DxfStatements(object):
         def gerbers():
             yield 'D{0}*'.format(self.dcode)
             if self.draw_mode == DxfFile.DM_FILL:
-                yield 'G36*'
                 for statement in self.statements:
+                    yield 'G36*'
                     if isinstance(statement, DxfCircleStatement) or \
                         (isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed):
                         yield statement.to_gerber(settings)
-                yield 'G37*'
+                    yield 'G37*'
+                for path in self.paths:
+                    yield 'G36*'
+                    yield path.to_gerber(settings)
+                    yield 'G37*'
             else:
                 for statement in self.statements:
                     yield statement.to_gerber(
@@ -310,15 +396,21 @@ class DxfStatements(object):
         if self._units == 'metric':
             self._units = 'inch'
             self.pitch = inch(self.pitch)
+            self.error_range = inch(self.error_range)
             for statement in self.statements:
                 statement.to_inch()
+            for path in self.paths:
+                path.to_inch()
 
     def to_metric(self):
         if self._units == 'inch':
             self._units = 'metric'
             self.pitch = metric(self.pitch)
+            self.error_range = metric(self.error_range)
             for statement in self.statements:
                 statement.to_metric()
+            for path in self.paths:
+                path.to_metric()
 
 class DxfHeaderStatement(object):
     def to_gerber(self, settings):
diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
new file mode 100644
index 0000000..b066b08
--- /dev/null
+++ b/gerberex/dxf_path.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+from gerber.utils import inch, metric, write_gerber_value
+from gerber.cam import FileSettings
+
+class DxfPath(object):
+    def __init__(self, statement, error_range=0):
+        self.statements = [statement]
+        self.error_range = error_range
+    
+    @property
+    def start(self):
+        return self.statements[0].start
+    
+    @property
+    def end(self):
+        return self.statements[-1].end
+
+    @property
+    def is_closed(self):
+        from gerberex.dxf import is_equal_point
+        return len(self.statements) > 1 and \
+               is_equal_point(self.start, self.end, self.error_range)
+    
+    def is_equal_to(self, target, error_range=0):
+        from gerberex.dxf import is_equal_point
+        if not isinstance(target, DxfPath):
+            return False
+        if len(self.statements) != len(target.statements):
+            return False
+        if is_equal_point(self.start, target.start, error_range) and \
+           is_equal_point(self.end, target.end, error_range):
+            for i in range(0, len(self.statements)):
+               if not self.statements[i].is_equal_to(target.statements[i], error_range):
+                   return False
+            return True
+        elif is_equal_point(self.start, target.end, error_range) and \
+             is_equal_point(self.end, target.start, error_range):
+            for i in range(0, len(self.statements)):
+               if not self.statements[i].is_equal_to(target.statements[-1 - i], error_range):
+                   return False
+            return True
+        return False
+
+    def to_inch(self):
+        self.error_range = inch(self.error_range)
+
+    def to_metric(self):
+        self.error_range = metric(self.error_range)
+
+    def reverse(self):
+        rlist = []
+        for statement in reversed(self.statements):
+            statement.reverse()
+            rlist.append(statement)
+        self.statements = rlist
+    
+    def merge(self, element, error_range=0):
+        from gerberex.dxf import is_equal_point
+        if self.is_closed or element.is_closed:
+            return False
+        if not error_range:
+            error_range = self.error_range
+        if is_equal_point(self.end, element.start, error_range):
+            return self._append_at_end(element, error_range)
+        elif is_equal_point(self.end, element.end, error_range):
+            element.reverse()
+            return self._append_at_end(element, error_range)
+        elif is_equal_point(self.start, element.end, error_range):
+            return self._insert_on_top(element, error_range)
+        elif is_equal_point(self.start, element.start, error_range):
+            element.reverse()
+            return self._insert_on_top(element, error_range)
+        else:
+            return False
+    
+    def _append_at_end(self, element, error_range=0):
+        if isinstance(element, DxfPath):
+            if self.is_equal_to(element, error_range):
+                return False
+            for i in range(0, min(len(self.statements), len(element.statements))):
+                if not self.statements[-1 - i].is_equal_to(element.statements[i]):
+                    break
+            for j in range(0, min(len(self.statements), len(element.statements))):
+                if not self.statements[j].is_equal_to(element.statements[-1 - j]):
+                    break
+            if i + j >= len(element.statements):
+                return False
+            mergee = list(element.statements)
+            if i > 0:
+                del mergee[0:i]
+                del self.statements[-i]
+            if j > 0:
+                del mergee[-j]
+                del self.statements[0:j]
+            self.statements.extend(mergee)
+            return True
+        else:
+            if self.statements[-1].is_equal_to(element, error_range) or \
+               self.statements[0].is_equal_to(element, error_range):
+                return False
+            self.statements.appen(element)
+            return True
+
+    def _insert_on_top(self, element, error_range=0):
+        if isinstance(element, DxfPath):
+            if self.is_equal_to(element, error_range):
+                return False
+            for i in range(0, min(len(self.statements), len(element.statements))):
+                if not self.statements[-1 - i].is_equal_to(element.statements[i]):
+                    break
+            for j in range(0, min(len(self.statements), len(element.statements))):
+                if not self.statements[j].is_equal_to(element.statements[-1 - j]):
+                    break
+            if i + j >= len(element.statements):
+                return False
+            mergee = list(element.statements)
+            if i > 0:
+                del mergee[0:i]
+                del self.statements[-i]
+            if j > 0:
+                del mergee[-j]
+                del self.statements[0:j]
+            self.statements[0:0] = mergee
+            return True
+        else:
+            if self.statements[-1].is_equal_to(element, error_range) or \
+               self.statements[0].is_equal_to(element, error_range):
+                return False
+            self.statements.insert(0, element)
+            return True
+
+    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
+        from gerberex.dxf import DxfArcStatement
+        if pitch:
+            return
+
+        x0, y0 = self.statements[0].start
+        gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
+            write_gerber_value(x0, settings.format,
+                               settings.zero_suppression),
+            write_gerber_value(y0, settings.format,
+                               settings.zero_suppression),
+        )
+
+        for statement in self.statements:
+            x0, y0 = statement.start
+            x1, y1 = statement.end
+            if isinstance(statement, DxfArcStatement):
+                xc, yc = statement.center
+                gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
+                    '03' if statement.end_angle > statement.start_angle else '02',
+                    write_gerber_value(x1, settings.format,
+                                       settings.zero_suppression),
+                    write_gerber_value(y1, settings.format,
+                                       settings.zero_suppression),
+                    write_gerber_value(xc - x0, settings.format,
+                                       settings.zero_suppression),
+                    write_gerber_value(yc - y0, settings.format,
+                                       settings.zero_suppression)
+                )
+            else:
+                gerber += '\nG01*\nX{0}Y{1}D01*'.format(
+                    write_gerber_value(x1, settings.format,
+                                       settings.zero_suppression),
+                    write_gerber_value(y1, settings.format,
+                                       settings.zero_suppression),
+                )
+
+        return gerber
+
+def generate_closed_paths(statements, error_range=0):
+    from gerberex.dxf import DxfLineStatement, DxfArcStatement
+
+    paths = [DxfPath(s, error_range) \
+             for s in filter(lambda s: isinstance(s, DxfLineStatement) or \
+                                       isinstance(s, DxfArcStatement), statements)]
+
+    prev_paths_num = 0
+    while prev_paths_num != len(paths):
+        working = []
+        for i in range(len(paths)):
+            mergee = paths[i]
+            for j in range(i + 1, len(paths) - 1):
+                target = paths[j]
+                if target.merge(mergee, error_range):
+                    break
+            else:
+                working.append(mergee)
+        prev_paths_num = len(paths)
+        paths = working
+    
+    return list(filter(lambda p: p.is_closed, paths))
+    
-- 
cgit 


From 53816574a986722d7af26c5597248d9c96f31bd3 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Sun, 31 Mar 2019 18:16:34 +0900
Subject: fix a minor issue

---
 gerberex/dxf.py      | 8 +++-----
 gerberex/dxf_path.py | 3 +--
 2 files changed, 4 insertions(+), 7 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index e6eb971..e863238 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -259,7 +259,7 @@ class DxfPolylineStatement(DxfStatement):
         if self.is_closed:
             self.end = self.start
         else:
-            self.start = (self.entity.points[-1][0], self.entity.points[-1][1])
+            self.end = (self.entity.points[-1][0], self.entity.points[-1][1])
 
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         if pitch:
@@ -364,16 +364,14 @@ class DxfStatements(object):
         def gerbers():
             yield 'D{0}*'.format(self.dcode)
             if self.draw_mode == DxfFile.DM_FILL:
+                yield 'G36*'
                 for statement in self.statements:
-                    yield 'G36*'
                     if isinstance(statement, DxfCircleStatement) or \
                         (isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed):
                         yield statement.to_gerber(settings)
-                    yield 'G37*'
                 for path in self.paths:
-                    yield 'G36*'
                     yield path.to_gerber(settings)
-                    yield 'G37*'
+                yield 'G37*'
             else:
                 for statement in self.statements:
                     yield statement.to_gerber(
diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
index b066b08..930d8d2 100644
--- a/gerberex/dxf_path.py
+++ b/gerberex/dxf_path.py
@@ -184,7 +184,7 @@ def generate_closed_paths(statements, error_range=0):
         working = []
         for i in range(len(paths)):
             mergee = paths[i]
-            for j in range(i + 1, len(paths) - 1):
+            for j in range(i + 1, len(paths)):
                 target = paths[j]
                 if target.merge(mergee, error_range):
                     break
@@ -192,6 +192,5 @@ def generate_closed_paths(statements, error_range=0):
                 working.append(mergee)
         prev_paths_num = len(paths)
         paths = working
-    
     return list(filter(lambda p: p.is_closed, paths))
     
-- 
cgit 


From eda75275505e14439e2dcd1990d2b95217546db1 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Mon, 1 Apr 2019 22:07:56 +0900
Subject: compliant with Python 2.7

---
 gerberex/dxf.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index e863238..1b2ddf7 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -3,7 +3,7 @@
 
 # Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
 
-import io
+import io, sys
 from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt
 import dxfgrabber
 from gerber.cam import CamFile, FileSettings
@@ -561,6 +561,8 @@ class DxfFile(CamFile):
         raise Exception('Not supported')
 
 def loads(data, filename=None):
+    if sys.version_info.major == 2:
+        data = unicode(data)
     stream = io.StringIO(data)
     dxf = dxfgrabber.read(stream)
     return DxfFile(dxf)
-- 
cgit 


From cb420e39e278f7ab6f002600a7698d7be101eb7d Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Wed, 3 Apr 2019 00:30:00 +0900
Subject: fix a rotaion issue

---
 gerberex/dxf.py      |  8 +-------
 gerberex/dxf_path.py |  4 +---
 gerberex/rs274x.py   | 16 +++++++++++++++-
 gerberex/utility.py  |  8 ++++++++
 4 files changed, 25 insertions(+), 11 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 1b2ddf7..39d256b 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -11,17 +11,11 @@ from gerber.utils import inch, metric, write_gerber_value
 from gerber.gerber_statements import ADParamStmt
 from gerber.excellon_statements import ExcellonTool
 from gerber.excellon_statements import CoordinateStmt
+from gerberex.utility import is_equal_point, is_equal_value
 from gerberex.dxf_path import generate_closed_paths
 
 ACCEPTABLE_ERROR = 0.001
 
-def is_equal_value(a, b, error_range=0):
-    return a - b <= error_range and a - b >= -error_range
-
-def is_equal_point(a, b, error_range=0):
-    return is_equal_value(a[0], b[0], error_range) and \
-           is_equal_value(a[1], b[1], error_range)
-
 class DxfStatement(object):
     def __init__(self, entity):
         self.entity = entity
diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
index 930d8d2..2825c58 100644
--- a/gerberex/dxf_path.py
+++ b/gerberex/dxf_path.py
@@ -5,6 +5,7 @@
 
 from gerber.utils import inch, metric, write_gerber_value
 from gerber.cam import FileSettings
+from gerberex.utility import is_equal_point, is_equal_value
 
 class DxfPath(object):
     def __init__(self, statement, error_range=0):
@@ -21,12 +22,10 @@ class DxfPath(object):
 
     @property
     def is_closed(self):
-        from gerberex.dxf import is_equal_point
         return len(self.statements) > 1 and \
                is_equal_point(self.start, self.end, self.error_range)
     
     def is_equal_to(self, target, error_range=0):
-        from gerberex.dxf import is_equal_point
         if not isinstance(target, DxfPath):
             return False
         if len(self.statements) != len(target.statements):
@@ -59,7 +58,6 @@ class DxfPath(object):
         self.statements = rlist
     
     def merge(self, element, error_range=0):
-        from gerberex.dxf import is_equal_point
         if self.is_closed or element.is_closed:
             return False
         if not error_range:
diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
index 4b477d3..4eb317d 100644
--- a/gerberex/rs274x.py
+++ b/gerberex/rs274x.py
@@ -30,11 +30,25 @@ class GerberFile(gerber.rs274x.GerberFile):
         if angle % 360 == 0:
             return
         self._generalize_aperture()
+        last_x = 0
+        last_y = 0
+        last_rx = 0
+        last_ry = 0
         for statement in self.statements:
             if isinstance(statement, AMParamStmtEx):
                 statement.rotate(angle, center)
             elif isinstance(statement, CoordStmt) and statement.x != None and statement.y != None:
-                statement.x, statement.y = rotate(statement.x, statement.y, angle, center)
+                if statement.i != None and statement.j != None:
+                    cx = last_x + statement.i
+                    cy = last_y + statement.j
+                    cx, cy = rotate(cx, cy, angle, center)
+                    statement.i = cx - last_rx
+                    statement.j = cy - last_ry
+                last_x = statement.x
+                last_y = statement.y
+                last_rx, last_ry = rotate(statement.x, statement.y, angle, center)
+                statement.x = last_rx
+                statement.y = last_ry
     
     def _generalize_aperture(self):
         RECTANGLE = 0
diff --git a/gerberex/utility.py b/gerberex/utility.py
index 852519a..f90df96 100644
--- a/gerberex/utility.py
+++ b/gerberex/utility.py
@@ -11,3 +11,11 @@ def rotate(x, y, angle, center):
     angle = angle * pi / 180.0
     return (cos(angle) * x0 - sin(angle) * y0 + center[0],
             sin(angle) * x0 + cos(angle) * y0 + center[1])
+
+def is_equal_value(a, b, error_range=0):
+    return a - b <= error_range and a - b >= -error_range
+
+
+def is_equal_point(a, b, error_range=0):
+    return is_equal_value(a[0], b[0], error_range) and \
+        is_equal_value(a[1], b[1], error_range)
-- 
cgit 


From 6b4603af21839c94a33d3804c364ff1b809eb341 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Wed, 3 Apr 2019 12:17:59 +0900
Subject: add rectangle generator

---
 gerberex/__init__.py |  4 +--
 gerberex/common.py   |  5 ++++
 gerberex/dxf.py      | 82 ++++++++++++++++++++++++++++++++++++----------------
 3 files changed, 64 insertions(+), 27 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/__init__.py b/gerberex/__init__.py
index cef11ab..f379c1c 100644
--- a/gerberex/__init__.py
+++ b/gerberex/__init__.py
@@ -10,6 +10,6 @@ gerber-tools-extension is a extention package for gerber-tools.
 This package provide panelizing of PCB fucntion.
 """
 
-from gerberex.common import (read, loads)
-from gerberex.composition import (GerberComposition, DrillComposition)
+from gerberex.common import read, loads, rectangle
+from gerberex.composition import GerberComposition, DrillComposition
 from gerberex.dxf import DxfFile
diff --git a/gerberex/common.py b/gerberex/common.py
index 4a85bd4..47bc6b3 100644
--- a/gerberex/common.py
+++ b/gerberex/common.py
@@ -33,3 +33,8 @@ def loads(data, filename=None, format=None):
         return ipc356.loads(data, filename=filename)
     else:
         raise ParseError('Unable to detect file format')
+
+
+def rectangle(width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None):
+    return gerberex.dxf.DxfFile.rectangle(
+        width, height, left, bottom, units, draw_mode, filename)
diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 39d256b..9a2186c 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -42,10 +42,16 @@ class DxfStatement(object):
         raise Exception('Not implemented')
 
 class DxfLineStatement(DxfStatement):
-    def __init__(self, entity):
+    @classmethod
+    def from_entity(cls, entity):
+        start = (entity.start[0], entity.start[1])
+        end = (entity.end[0], entity.end[1])
+        return cls(entity, start, end)
+
+    def __init__(self, entity, start, end):
         super(DxfLineStatement, self).__init__(entity)
-        self.start = (self.entity.start[0], self.entity.start[1])
-        self.end = (self.entity.end[0], self.entity.end[1])
+        self.start = start
+        self.end = end
     
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         if pitch == 0:
@@ -329,7 +335,7 @@ class DxfPolylineStatement(DxfStatement):
             self.entity.bulge[idx] = metric(self.entity.bulge[idx])
 
 class DxfStatements(object):
-    def __init__(self, entities, units, dcode=10, draw_mode=None):
+    def __init__(self, statements, units, dcode=10, draw_mode=None):
         if draw_mode == None:
             draw_mode = DxfFile.DM_LINE
         self._units = units
@@ -338,16 +344,7 @@ class DxfStatements(object):
         self.pitch = inch(1) if self._units == 'inch' else 1
         self.width = 0
         self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR
-        self.statements = []
-        for entity in entities:
-            if entity.dxftype == 'LWPOLYLINE':
-                self.statements.append(DxfPolylineStatement(entity))
-            elif entity.dxftype == 'LINE':
-                self.statements.append(DxfLineStatement(entity))
-            elif entity.dxftype == 'CIRCLE':
-                self.statements.append(DxfCircleStatement(entity))
-            elif entity.dxftype == 'ARC':
-                self.statements.append(DxfArcStatement(entity))
+        self.statements = statements
         self.paths = generate_closed_paths(self.statements, self.error_range)
 
     @property
@@ -458,18 +455,52 @@ class DxfFile(CamFile):
     FT_RX274X = 0
     FT_EXCELLON = 1
 
-    def __init__(self, dxf, settings=None, draw_mode=None, filename=None):
-        if not settings:
-            settings = FileSettings(zero_suppression='leading')
+    @classmethod
+    def from_dxf(cls, dxf, settings=None, draw_mode=None, filename=None):
+        fsettings = settings if settings else \
+            FileSettings(zero_suppression='leading')
 
-        if draw_mode == None:
-            draw_mode = self.DM_LINE
         if dxf.header['$INSUNITS'] == 1:
-            settings.units = 'inch'
-            settings.format = (2, 5)
+            fsettings.units = 'inch'
+            if not settings:
+                fsettings.format = (2, 5)
+        else:
+            fsettings.units = 'metric'
+            if not settings:
+                fsettings.format = (3, 4)
+
+        statements = []
+        for entity in dxf.entities:
+            if entity.dxftype == 'LWPOLYLINE':
+                statements.append(DxfPolylineStatement(entity))
+            elif entity.dxftype == 'LINE':
+                statements.append(DxfLineStatement.from_entity(entity))
+            elif entity.dxftype == 'CIRCLE':
+                statements.append(DxfCircleStatement(entity))
+            elif entity.dxftype == 'ARC':
+                statements.append(DxfArcStatement(entity))
+        
+        return cls(statements, fsettings, draw_mode, filename)
+    
+    @classmethod
+    def rectangle(cls, width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None):
+        if units == 'metric':
+            settings = FileSettings(units=units, zero_suppression='leading', format=(3,4))
         else:
-            settings.units = 'metric'
-            settings.format = (3, 4)
+            settings = FileSettings(units=units, zero_suppression='leading', format=(2,5))
+        statements = [
+            DxfLineStatement(None, (left, bottom), (left + width, bottom)),
+            DxfLineStatement(None, (left + width, bottom), (left + width, bottom + height)),
+            DxfLineStatement(None, (left + width, bottom + height), (left, bottom + height)),
+            DxfLineStatement(None, (left, bottom + height), (left, bottom)),
+        ]
+        return cls(statements, settings, draw_mode, filename)
+
+    def __init__(self, statements, settings=None, draw_mode=None, filename=None):
+        if not settings:
+            settings = FileSettings(units='metric', format=(3,4), zero_suppression='leading')
+        if draw_mode == None:
+            draw_mode = self.DM_LINE
 
         super(DxfFile, self).__init__(settings=settings, filename=filename)
         self._draw_mode = draw_mode
@@ -477,7 +508,8 @@ class DxfFile(CamFile):
         
         self.header2 = DxfHeader2Statement()
         self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
-        self.statements = DxfStatements(dxf.entities, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
+        self.statements = DxfStatements(
+            statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
 
     @property
     def dcode(self):
@@ -559,4 +591,4 @@ def loads(data, filename=None):
         data = unicode(data)
     stream = io.StringIO(data)
     dxf = dxfgrabber.read(stream)
-    return DxfFile(dxf)
+    return DxfFile.from_dxf(dxf)
-- 
cgit 


From d53293a609a83aa945af6864285b90d36bcbdd69 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Wed, 3 Apr 2019 19:05:20 +0900
Subject: add move and rotation capability to DxfFile object

---
 gerberex/dxf.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 59 insertions(+), 7 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 9a2186c..ba02f08 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -7,7 +7,7 @@ import io, sys
 from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt
 import dxfgrabber
 from gerber.cam import CamFile, FileSettings
-from gerber.utils import inch, metric, write_gerber_value
+from gerber.utils import inch, metric, write_gerber_value, rotate_point
 from gerber.gerber_statements import ADParamStmt
 from gerber.excellon_statements import ExcellonTool
 from gerber.excellon_statements import CoordinateStmt
@@ -41,6 +41,13 @@ class DxfStatement(object):
     def reverse(self):
         raise Exception('Not implemented')
 
+    def offset(self, offset_x, offset_y):
+        raise Exception('Not supported')
+    
+    def rotate(self, angle, center=(0, 0)):
+        raise Exception('Not supported')
+
+
 class DxfLineStatement(DxfStatement):
     @classmethod
     def from_entity(cls, entity):
@@ -93,9 +100,9 @@ class DxfLineStatement(DxfStatement):
 
     def to_metric(self):
         self.start = (
-            metric(self.start[0]), inch(self.start[1]))
+            metric(self.start[0]), metric(self.start[1]))
         self.end = (
-            metric(self.end[0]), inch(self.end[1]))
+            metric(self.end[0]), metric(self.end[1]))
 
     def is_equal_to(self, target, error_range=0):
         if not isinstance(target, DxfLineStatement):
@@ -127,6 +134,14 @@ class DxfLineStatement(DxfStatement):
             y0 += yd
             d += pitch
 
+    def offset(self, offset_x, offset_y):
+        self.start = (self.start[0] + offset_x, self.start[1] + offset_y)
+        self.end = (self.end[0] + offset_x, self.end[1] + offset_y)
+
+    def rotate(self, angle, center=(0, 0)):
+        self.start = rotate_point(self.start, angle, center)
+        self.end = rotate_point(self.end, angle, center)
+
 class DxfCircleStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfCircleStatement, self).__init__(entity)
@@ -177,6 +192,12 @@ class DxfCircleStatement(DxfStatement):
     def reverse(self):
         pass
 
+    def offset(self, offset_x, offset_y):
+        self.center = (self.center[0] + offset_x, self.center[1] + offset_y)
+
+    def rotate(self, angle, center=(0, 0)):
+        self.center = rotate_point(self.center, angle, center)
+
 class DxfArcStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfArcStatement, self).__init__(entity)
@@ -251,6 +272,18 @@ class DxfArcStatement(DxfStatement):
         self.start = self.end
         self.end = tmp
 
+    def offset(self, offset_x, offset_y):
+        self.center = (self.center[0] + offset_x, self.center[1] + offset_y)
+        self.start = (self.start[0] + offset_x, self.start[1] + offset_y)
+        self.end = (self.end[0] + offset_x, self.end[1] + offset_y)
+
+    def rotate(self, angle, center=(0, 0)):
+        self.start_angle += angle
+        self.end_angle += angle
+        self.center = rotate_point(self.center, angle, center)
+        self.start = rotate_point(self.start, angle, center)
+        self.end = rotate_point(self.end, angle, center)
+
 class DxfPolylineStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfPolylineStatement, self).__init__(entity)
@@ -324,7 +357,6 @@ class DxfPolylineStatement(DxfStatement):
         for idx in range(0, len(self.entity.points)):
             self.entity.points[idx] = (
                 inch(self.entity.points[idx][0]), inch(self.entity.points[idx][1]))
-            self.entity.bulge[idx] = inch(self.entity.bulge[idx])
 
     def to_metric(self):
         self.start = (metric(self.start[0]), metric(self.start[1]))
@@ -332,7 +364,16 @@ class DxfPolylineStatement(DxfStatement):
         for idx in range(0, len(self.entity.points)):
             self.entity.points[idx] = (
                 metric(self.entity.points[idx][0]), metric(self.entity.points[idx][1]))
-            self.entity.bulge[idx] = metric(self.entity.bulge[idx])
+    
+    def offset(self, offset_x, offset_y):
+        for idx in range(len(self.entity.points)):
+            self.entity.points[idx] = (
+                self.entity.points[idx][0] + offset_x, self.entity.points[idx][1] + offset_y)
+
+    def rotate(self, angle, center=(0, 0)):
+        for idx in range(len(self.entity.points)):
+            self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center)
+
 
 class DxfStatements(object):
     def __init__(self, statements, units, dcode=10, draw_mode=None):
@@ -400,6 +441,14 @@ class DxfStatements(object):
                 statement.to_metric()
             for path in self.paths:
                 path.to_metric()
+    
+    def offset(self, offset_x, offset_y):
+        for statement in self.statements:
+            statement.offset(offset_x, offset_y)
+
+    def rotate(self, angle, center=(0, 0)):
+        for statement in self.statements:
+            statement.rotate(angle, center)
 
 class DxfHeaderStatement(object):
     def to_gerber(self, settings):
@@ -583,8 +632,11 @@ class DxfFile(CamFile):
             self.pitch = metric(self.pitch)
             self.units = 'metric'
 
-    def offset(self, ofset_x, offset_y):
-        raise Exception('Not supported')
+    def offset(self, offset_x, offset_y):
+        self.statements.offset(offset_x, offset_y)
+    
+    def rotate(self, angle, center=(0, 0)):
+        self.statements.rotate(angle, center)
 
 def loads(data, filename=None):
     if sys.version_info.major == 2:
-- 
cgit 


From e3c59e39cf9bc64ce9d76c324b82956a65515f16 Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Sun, 7 Apr 2019 22:22:33 +0900
Subject: expand test and fix many issues

---
 gerberex/am_expression.py     |   4 +-
 gerberex/am_primitive.py      |  16 +++---
 gerberex/composition.py       |   4 +-
 gerberex/excellon.py          |  17 ++++++-
 gerberex/gerber_statements.py | 115 ++++++++++++++++++++++++++++++++++++++++++
 gerberex/rs274x.py            |  26 +++++++---
 gerberex/statements.py        |  70 -------------------------
 gerberex/utility.py           |   3 +-
 8 files changed, 163 insertions(+), 92 deletions(-)
 create mode 100644 gerberex/gerber_statements.py
 delete mode 100644 gerberex/statements.py

(limited to 'gerberex')

diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py
index 7aa95c8..b758df1 100644
--- a/gerberex/am_expression.py
+++ b/gerberex/am_expression.py
@@ -49,7 +49,7 @@ class AMConstantExpression(AMExpression):
         return self
     
     def to_gerber(self, settings=None):
-        return str(self._value)
+        return '%.6g' % self._value
 
     def to_instructions(self):
         return [(OpCode.PUSH, self._value)]
@@ -179,5 +179,3 @@ def eval_macro(instructions):
         elif opcode == OpCode.PRIM:
             yield (argument, stack)
             stack = []
-
-
diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py
index 82370f6..df55573 100644
--- a/gerberex/am_primitive.py
+++ b/gerberex/am_primitive.py
@@ -117,14 +117,14 @@ class AMVectorLinePrimitiveDef(AMPrimitiveDef):
         self.start_x = self.start_x.to_inch().optimize()
         self.start_y = self.start_y.to_inch().optimize()
         self.end_x = self.end_x.to_inch().optimize()
-        self.end_y = self.end_x.to_inch().optimize()
+        self.end_y = self.end_y.to_inch().optimize()
     
     def to_metric(self):
         self.width = self.width.to_metric().optimize()
         self.start_x = self.start_x.to_metric().optimize()
         self.start_y = self.start_y.to_metric().optimize()
         self.end_x = self.end_x.to_metric().optimize()
-        self.end_y = self.end_x.to_metric().optimize()
+        self.end_y = self.end_y.to_metric().optimize()
 
     def to_gerber(self, settings=None):
         data = dict(code = self.code,
@@ -197,11 +197,11 @@ class AMCenterLinePrimitiveDef(AMPrimitiveDef):
 class AMOutlinePrimitiveDef(AMPrimitiveDef):
     @classmethod
     def from_modifiers(cls, code, modifiers):
-        num_points = modifiers[1] + 1
+        num_points = int(modifiers[1].value + 1)
         code = code
         exposure = 'on' if modifiers[0].value == 1 else 'off'
-        addrs = modifiers[2:num_points * 2]
-        rotation = modifiers[3 + num_points * 2]
+        addrs = modifiers[2:num_points * 2 + 2]
+        rotation = modifiers[2 + num_points * 2]
         return cls(code, exposure, addrs, rotation)
 
     def __init__(self, code, exposure, addrs, rotation):
@@ -209,10 +209,10 @@ class AMOutlinePrimitiveDef(AMPrimitiveDef):
         self.addrs = addrs
 
     def to_inch(self):
-        self.addrs = [i.to_inch() for i in self.addrs]
+        self.addrs = [i.to_inch().optimize() for i in self.addrs]
     
     def to_metric(self):
-        self.addrs = [i.to_metric() for i in self.addrs]
+        self.addrs = [i.to_metric().optimize() for i in self.addrs]
  
     def to_gerber(self, settings=None):
         def strs():
@@ -262,7 +262,7 @@ class AMPolygonPrimitiveDef(AMPrimitiveDef):
     def to_metric(self):
         self.x = self.x.to_metric().optimize()
         self.y = self.y.to_metric().optimize()
-        self.diameter = self.diameter.to_inch().optimize()
+        self.diameter = self.diameter.to_metric().optimize()
 
     def to_gerber(self, settings=None):
         data = dict(code = self.code,
diff --git a/gerberex/composition.py b/gerberex/composition.py
index 73f5702..7abf090 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -202,8 +202,8 @@ class DrillComposition(Composition):
                 file.to_inch()
 
         if not self.header1_statements:
-            self.header1_statements = file.header
-            self.header2_statements = file.header2
+            self.header1_statements = [file.header]
+            self.header2_statements = [file.header2]
         
         tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width))
         self.dxf_statements.append((tool.number, file.statements))
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 90d6742..b72b95b 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -6,6 +6,7 @@
 from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile)
 from gerber.excellon_statements import UnitStmt
 from gerber.cam import FileSettings
+from gerber.utils import inch, metric
 from gerberex.utility import rotate
 
 def loads(data, filename=None, settings=None, tools=None, format=None):
@@ -33,6 +34,19 @@ class ExcellonFileEx(ExcellonFile):
             return
         for hit in self.hits:
             hit.position = rotate(hit.position[0], hit.position[1], angle, center)
+    
+    def to_inch(self):
+        if self.units == 'metric':
+            super(ExcellonFileEx, self).to_inch()
+            for hit in self.hits:
+                hit.position = (inch(hit.position[0]), inch(hit.position[1]))
+
+    def to_metric(self):
+        if self.units == 'inch':
+            super(ExcellonFileEx, self).to_metric()
+            for hit in self.hits:
+                hit.position = (metric(hit.position[0]), metric(hit.position[1]))
+
 
 class UnitStmtEx(UnitStmt):
     @classmethod
@@ -43,7 +57,8 @@ class UnitStmtEx(UnitStmt):
         super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs)
     
     def to_excellon(self, settings=None):
+        format = settings.format if settings else self.format
         stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
                           'LZ' if self.zeros == 'leading' else 'TZ',
-                          '0' * self.format[0], '0' * self.format[1])
+                          '0' * format[0], '0' * format[1])
         return stmt
diff --git a/gerberex/gerber_statements.py b/gerberex/gerber_statements.py
new file mode 100644
index 0000000..c2eb565
--- /dev/null
+++ b/gerberex/gerber_statements.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+from gerber.gerber_statements import AMParamStmt, ADParamStmt
+from gerber.utils import inch, metric
+from gerberex.am_primitive import to_primitive_defs
+
+class AMParamStmtEx(AMParamStmt):
+    @classmethod
+    def from_stmt(cls, stmt):
+        return cls(stmt.param, stmt.name, stmt.macro, stmt.units)
+
+    @classmethod
+    def circle(cls, name, units):
+        return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0', units)
+
+    @classmethod
+    def rectangle(cls, name, units):
+        return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0', units)
+    
+    @classmethod
+    def landscape_obround(cls, name, units):
+        return cls(
+            'AM', name,
+            '$4=$1-$2*'
+            '$5=$1-$4*'
+            '21,1,$5,$2,0,0,0*'
+            '1,1,$4,$4/2,0,0*'
+            '1,1,$4,-$4/2,0,0*'
+            '1,0,$3,0,0,0', units)
+
+    @classmethod
+    def portrate_obround(cls, name, units):
+        return cls(
+            'AM', name,
+            '$4=$2-$1*'
+            '$5=$2-$4*'
+            '21,1,$1,$5,0,0,0*'
+            '1,1,$4,0,$4/2,0*'
+            '1,1,$4,0,-$4/2,0*'
+            '1,0,$3,0,0,0', units)
+    
+    @classmethod
+    def polygon(cls, name, units):
+        return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0', units)
+
+    def __init__(self, param, name, macro, units):
+        super(AMParamStmtEx, self).__init__(param, name, macro)
+        self.units = units
+        self.primitive_defs = list(to_primitive_defs(self.instructions))
+    
+    def to_inch(self):
+        if self.units == 'metric':
+            self.units = 'inch'
+            for p in self.primitive_defs:
+                p.to_inch()
+
+    def to_metric(self):
+        if self.units == 'inch':
+            self.units = 'metric'
+            for p in self.primitive_defs:
+                p.to_metric()
+
+    def to_gerber(self, settings = None):
+        def plist():
+            for p in self.primitive_defs:
+                yield p.to_gerber(settings)
+        return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))
+
+    def rotate(self, angle, center=None):
+        for primitive_def in self.primitive_defs:
+            primitive_def.rotate(angle, center)
+
+class ADParamStmtEx(ADParamStmt):
+    GEOMETRIES = {
+        'C': [0,1],
+        'R': [0,1,2],
+        'O': [0,1,2],
+        'P': [0,3],
+    }
+
+    @classmethod
+    def from_stmt(cls, stmt):
+        modstr = ','.join([
+            'X'.join(['{0}'.format(x) for x in modifier])
+        for modifier in stmt.modifiers])
+        return cls(stmt.param, stmt.d, stmt.shape, modstr, stmt.units)
+
+    def __init__(self, param, d, shape, modifiers, units):
+        super(ADParamStmtEx, self).__init__(param, d, shape, modifiers)
+        self.units = units
+
+    def to_inch(self):
+        if self.units == 'inch':
+            return
+        self.units = 'inch'
+        if self.shape in self.GEOMETRIES:
+            indices = self.GEOMETRIES[self.shape]
+            self.modifiers = [tuple([
+                inch(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \
+                    for i in range(len(self.modifiers[0]))
+            ])]
+
+    def to_metric(self):
+        if self.units == 'metric':
+            return
+        self.units = 'metric'
+        if self.shape in self.GEOMETRIES:
+            indices = self.GEOMETRIES[self.shape]
+            self.modifiers = [tuple([
+                metric(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \
+                    for i in range(len(self.modifiers[0]))
+            ])]
diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
index 4eb317d..13d3421 100644
--- a/gerberex/rs274x.py
+++ b/gerberex/rs274x.py
@@ -5,7 +5,7 @@
 
 import gerber.rs274x
 from gerber.gerber_statements import ADParamStmt, CoordStmt
-from gerberex.statements import AMParamStmt, AMParamStmtEx
+from gerberex.gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx
 from gerberex.utility import rotate
 
 class GerberFile(gerber.rs274x.GerberFile):
@@ -17,6 +17,8 @@ class GerberFile(gerber.rs274x.GerberFile):
         def swap_statement(statement):
             if isinstance(statement, AMParamStmt) and not isinstance(statement, AMParamStmtEx):
                 return AMParamStmtEx.from_stmt(statement)
+            elif isinstance(statement, ADParamStmt) and not isinstance(statement, AMParamStmtEx):
+                return ADParamStmtEx.from_stmt(statement)
             else:
                 return statement
         statements = [swap_statement(statement) for statement in gerber_file.statements]
@@ -26,6 +28,18 @@ class GerberFile(gerber.rs274x.GerberFile):
     def __init__(self, statements, settings, primitives, apertures, filename=None):
         super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename)
 
+    def offset(self, x_offset=0, y_offset=0):
+        for statement in self.statements:
+            if isinstance(statement, CoordStmt):
+                if statement.x is not None:
+                    statement.x += x_offset
+                if statement.y is not None:
+                    statement.y += y_offset
+            else:
+                statement.offset(x_offset, y_offset)
+        for primitive in self.primitives:
+            primitive.offset(x_offset, y_offset)
+
     def rotate(self, angle, center=(0,0)):
         if angle % 360 == 0:
             return
@@ -84,7 +98,7 @@ class GerberFile(gerber.rs274x.GerberFile):
                 while name in macros:
                     name = '%s_%d' % (macro_def[0], num)
                     num += 1
-                self.statements.insert(insert_point, macro_def[1](name))
+                self.statements.insert(insert_point, macro_def[1](name, self.units))
                 macro_defs[idx] = (name, macro_def[1])
             for idx in range(insert_point, last_aperture + len(macro_defs) + 1):
                 statement = self.statements[idx]
@@ -92,10 +106,10 @@ class GerberFile(gerber.rs274x.GerberFile):
                     if statement.shape == 'R':
                         statement.shape = macro_defs[RECTANGLE][0]
                     elif statement.shape == 'O':
-                        x = statement.modifiers[0] \
-                            if len(statement.modifiers) > 0 else 0
-                        y = statement.modifiers[1] \
-                            if len(statement.modifiers) > 1 else 0
+                        x = statement.modifiers[0][0] \
+                            if len(statement.modifiers[0]) > 0 else 0
+                        y = statement.modifiers[0][1] \
+                            if len(statement.modifiers[0]) > 1 else 0
                         statement.shape = macro_defs[LANDSCAPE_OBROUND][0] \
                                           if x > y else macro_defs[PORTRATE_OBROUND][0] 
                     elif statement.shape == 'P':
diff --git a/gerberex/statements.py b/gerberex/statements.py
deleted file mode 100644
index c41acb9..0000000
--- a/gerberex/statements.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
-
-from gerber.gerber_statements import AMParamStmt
-from gerberex.am_primitive import to_primitive_defs
-
-class AMParamStmtEx(AMParamStmt):
-    @classmethod
-    def from_stmt(cls, stmt):
-        return cls(stmt.param, stmt.name, stmt.macro)
-
-    @classmethod
-    def circle(cls, name):
-        return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0')
-
-    @classmethod
-    def rectangle(cls, name):
-        return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0')
-    
-    @classmethod
-    def landscape_obround(cls, name):
-        return cls(
-            'AM', name,
-            '$4=$1-$2*'
-            '21,1,$1-$4,$2,0,0,0*'
-            '1,1,$4,$4/2,0,0*'
-            '1,1,$4,-$4/2,0,0*'
-            '1,0,$3,0,0,0')
-
-    @classmethod
-    def portrate_obround(cls, name):
-        return cls(
-            'AM', name,
-            '$4=$2-$1*'
-            '21,1,$1,$2-$4,0,0,0*'
-            '1,1,$4,0,$4/2,0*'
-            '1,1,$4,0,-$4/2,0*'
-            '1,0,$3,0,0,0')
-    
-    @classmethod
-    def polygon(cls, name):
-        return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0')
-
-    def __init__(self, param, name, macro):
-        super(AMParamStmtEx, self).__init__(param, name, macro)
-        self.primitive_defs = list(to_primitive_defs(self.instructions))
-    
-    def to_inch(self):
-        if self.units == 'metric':
-            self.units = 'inch'
-            for p in self.primitive_defs:
-                p.to_inch()
-
-    def to_metric(self):
-        if self.units == 'inch':
-            self.units = 'metric'
-            for p in self.primitive_defs:
-                p.to_metric()
-
-    def to_gerber(self, settings = None):
-        def plist():
-            for p in self.primitive_defs:
-                yield p.to_gerber(settings)
-        return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))
-
-    def rotate(self, angle, center=None):
-        for primitive_def in self.primitive_defs:
-            primitive_def.rotate(angle, center)
diff --git a/gerberex/utility.py b/gerberex/utility.py
index f90df96..4c89fa6 100644
--- a/gerberex/utility.py
+++ b/gerberex/utility.py
@@ -13,8 +13,7 @@ def rotate(x, y, angle, center):
             sin(angle) * x0 + cos(angle) * y0 + center[1])
 
 def is_equal_value(a, b, error_range=0):
-    return a - b <= error_range and a - b >= -error_range
-
+    return (a - b) * (a - b) <= error_range * error_range
 
 def is_equal_point(a, b, error_range=0):
     return is_equal_value(a[0], b[0], error_range) and \
-- 
cgit 


From 6ec58842030b0bbe6aa3391456f017d71b74aeaa Mon Sep 17 00:00:00 2001
From: opiopan <opiopan@gmail.com>
Date: Wed, 24 Apr 2019 22:55:23 +0900
Subject: fix a genarating closed path issue

---
 gerberex/dxf_path.py | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
index 2825c58..ca48d00 100644
--- a/gerberex/dxf_path.py
+++ b/gerberex/dxf_path.py
@@ -173,9 +173,20 @@ class DxfPath(object):
 def generate_closed_paths(statements, error_range=0):
     from gerberex.dxf import DxfLineStatement, DxfArcStatement
 
-    paths = [DxfPath(s, error_range) \
-             for s in filter(lambda s: isinstance(s, DxfLineStatement) or \
-                                       isinstance(s, DxfArcStatement), statements)]
+    unique_statements = []
+    redundant = 0
+    for statement in statements:
+        for target in unique_statements:
+            if not isinstance(statement, DxfLineStatement) and \
+               not isinstance(statement, DxfArcStatement):
+                break
+            if statement.is_equal_to(target, error_range):
+                redundant += 1
+                break
+        else:
+            unique_statements.append(statement)
+
+    paths = [DxfPath(s, error_range) for s in unique_statements]
 
     prev_paths_num = 0
     while prev_paths_num != len(paths):
-- 
cgit 


From 89b5b714c9d2e00ebcf849783fc30bada956dd59 Mon Sep 17 00:00:00 2001
From: Marin Mikaël <41113988+MarinMikael@users.noreply.github.com>
Date: Wed, 24 Jul 2019 01:19:09 +0900
Subject: Update composition.py

Add DrillSlot support to excellon composition.
---
 gerberex/composition.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

(limited to 'gerberex')

diff --git a/gerberex/composition.py b/gerberex/composition.py
index 7abf090..29725ba 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -7,6 +7,7 @@ from functools import reduce
 from gerber.cam import FileSettings
 from gerber.gerber_statements import EofStmt
 from gerber.excellon_statements import *
+from gerber.excellon import DrillSlot, DrillHit
 import gerberex.rs274x
 import gerberex.excellon
 import gerberex.dxf
@@ -149,7 +150,10 @@ class DrillComposition(Composition):
                 yield ToolSelectionStmt(t.number).to_excellon(self.settings)
                 for h in self.hits:
                     if h.tool.number == t.number:
-                        yield CoordinateStmt(*h.position).to_excellon(self.settings)
+                        if type(h) == DrillSlot:
+                            yield SlotStmt(*h.start, *h.end).to_excellon(self.settings)
+                        elif type(h) == DrillHit:
+                            yield CoordinateStmt(*h.position).to_excellon(self.settings)
                 for num, statement in self.dxf_statements:
                     if num == t.number:
                         yield statement.to_excellon(self.settings)
-- 
cgit 


From 415bdbc2e4339d370afcd779344ea66e39ba6b0a Mon Sep 17 00:00:00 2001
From: Marin Mikaël <41113988+MarinMikael@users.noreply.github.com>
Date: Wed, 24 Jul 2019 01:20:15 +0900
Subject: Update am_primitive.py

Fix bug when circle doesn't have any rotation by adding a default 0 degree rotation.
---
 gerberex/am_primitive.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'gerberex')

diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py
index df55573..a3ad824 100644
--- a/gerberex/am_primitive.py
+++ b/gerberex/am_primitive.py
@@ -56,7 +56,7 @@ class AMCirclePrimitiveDef(AMPrimitiveDef):
         diameter = modifiers[1]
         center_x = modifiers[2]
         center_y = modifiers[3]
-        rotation = modifiers[4]
+        rotation = modifiers[4] if len(modifiers)>4 else AMConstantExpression(float(0))
         return cls(code, exposure, diameter, center_x, center_y, rotation)
 
     def __init__(self, code, exposure, diameter, center_x, center_y, rotation):
-- 
cgit 


From 0854d357587c96adb881255d0af6a7d40c15153c Mon Sep 17 00:00:00 2001
From: Marin Mikaël <41113988+MarinMikael@users.noreply.github.com>
Date: Wed, 24 Jul 2019 01:24:41 +0900
Subject: Update am_expression.py

String format to %f instead of %g. Scientific notation is supportd by Kicad and Ucamco's reference viewer, but causes artefacts on Altium and pcb-tools.
---
 gerberex/am_expression.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'gerberex')

diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py
index b758df1..4bb1d78 100644
--- a/gerberex/am_expression.py
+++ b/gerberex/am_expression.py
@@ -49,7 +49,7 @@ class AMConstantExpression(AMExpression):
         return self
     
     def to_gerber(self, settings=None):
-        return '%.6g' % self._value
+        return '%.6f' % self._value
 
     def to_instructions(self):
         return [(OpCode.PUSH, self._value)]
-- 
cgit 


From 22f4c8a3f5bdce243908f3787216344b200902df Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sat, 17 Aug 2019 23:38:30 +0900
Subject: router mode and G85 slot in excellon file is supported

---
 gerberex/composition.py |   5 +-
 gerberex/excellon.py    | 251 +++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 241 insertions(+), 15 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/composition.py b/gerberex/composition.py
index 29725ba..7f691f5 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -150,10 +150,7 @@ class DrillComposition(Composition):
                 yield ToolSelectionStmt(t.number).to_excellon(self.settings)
                 for h in self.hits:
                     if h.tool.number == t.number:
-                        if type(h) == DrillSlot:
-                            yield SlotStmt(*h.start, *h.end).to_excellon(self.settings)
-                        elif type(h) == DrillHit:
-                            yield CoordinateStmt(*h.position).to_excellon(self.settings)
+                        yield h.to_excellon(self.settings)
                 for num, statement in self.dxf_statements:
                     if num == t.number:
                         yield statement.to_excellon(self.settings)
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index b72b95b..657f02a 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -3,10 +3,15 @@
 
 # Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
 
-from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile)
-from gerber.excellon_statements import UnitStmt
+import operator
+
+from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot
+from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \
+                                       ToolSelectionStmt, ZAxisRoutPositionStmt, \
+                                       RetractWithClampingStmt, RetractWithoutClampingStmt, \
+                                       EndOfProgramStmt
 from gerber.cam import FileSettings
-from gerber.utils import inch, metric
+from gerber.utils import inch, metric, write_gerber_value, parse_gerber_value
 from gerberex.utility import rotate
 
 def loads(data, filename=None, settings=None, tools=None, format=None):
@@ -20,11 +25,93 @@ def loads(data, filename=None, settings=None, tools=None, format=None):
 class ExcellonFileEx(ExcellonFile):
     @classmethod
     def from_file(cls, file):
-        statements = [
-            UnitStmtEx.from_statement(s) if isinstance(s, UnitStmt) else s \
-                for s in file.statements
-        ]
-        return cls(statements, file.tools, file.hits, file.settings, file.filename)
+        def correct_statements():
+            lazy_stmt = None
+            modifier = None
+            for stmt in file.statements:
+                modifier = lazy_stmt
+                lazy_stmt = None
+                if isinstance(stmt, UnitStmt):
+                    yield UnitStmtEx.from_statement(stmt)
+                elif isinstance(stmt, CoordinateStmt):
+                    new_stmt = CoordinateStmtEx.from_statement(stmt)
+                    if modifier and new_stmt.mode is None:
+                        new_stmt.mode = modifier
+                    yield new_stmt
+                elif isinstance(stmt, UnknownStmt):
+                    line = stmt.stmt.strip()
+                    mode = None
+                    if line[:3] == 'G02':
+                        mode = CoordinateStmtEx.MODE_CIRCULER_CW
+                    elif line[:3] == 'G03':
+                        mode = CoordinateStmtEx.MODE_CIRCULER_CCW
+                    else:
+                        yield stmt
+                        continue
+                    if len(line) == 3:
+                        lazy_stmt = mode
+                        continue
+                    new_stmt = CoordinateStmtEx.from_excellon(line[3:], file.settings)
+                    new_stmt.mode = mode
+                    yield new_stmt
+                else:
+                    yield stmt
+
+        def generate_hits(statements):
+            STAT_DRILL = 0
+            STAT_ROUT_UP = 1
+            STAT_ROUT_DOWN = 2
+            status = STAT_DRILL
+            current_tool = None
+            rout_statements = []
+
+            def make_rout(status, statements):
+                if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None:
+                    return None
+                return DrillRout.from_coordinates(current_tool, statements)
+
+            for stmt in statements:
+                if isinstance(stmt, ToolSelectionStmt):
+                    current_tool = file.tools[stmt.tool]
+                elif isinstance(stmt, DrillModeStmt):
+                    rout = make_rout(status, rout_statements)
+                    rout_statements = []
+                    if rout is not None:
+                        yield rout
+                    status = STAT_DRILL
+                elif isinstance(stmt, ZAxisRoutPositionStmt) and status == STAT_ROUT_UP:
+                    status = STAT_ROUT_DOWN
+                elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt):
+                    rout = make_rout(status, rout_statements)
+                    rout_statements = []
+                    if rout is not None:
+                        yield rout
+                    status = STAT_ROUT_UP
+                elif isinstance(stmt, SlotStmt):
+                    yield DrillSlotEx(current_tool, (stmt.x_start, stmt.y_start), 
+                                      (stmt.x_end, stmt.y_end), DrillSlotEx.TYPE_G85)
+                elif isinstance(stmt, CoordinateStmtEx):
+                    if stmt.mode is None:
+                        if status != STAT_DRILL:
+                            raise Exception('invalid statement sequence')
+                        yield DrillHitEx(current_tool, (stmt.x, stmt.y))
+                    else:
+                        if stmt.mode == stmt.MODE_ROUT:
+                            status = STAT_ROUT_UP
+                        if status == STAT_ROUT_UP:
+                            rout_statements = [stmt]
+                        elif status == STAT_ROUT_DOWN:
+                            rout_statements.append(stmt)
+                        else:
+                            raise Exception('invalid statement sequence')
+
+        statements = [s for s in correct_statements()]
+        hits = [h for h in generate_hits(statements)]
+        return cls(statements, file.tools, hits, file.settings, file.filename)
+    
+    @property
+    def primitives(self):
+        return []
 
     def __init__(self, statements, tools, hits, settings, filename=None):
         super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename)
@@ -33,20 +120,102 @@ class ExcellonFileEx(ExcellonFile):
         if angle % 360 == 0:
             return
         for hit in self.hits:
-            hit.position = rotate(hit.position[0], hit.position[1], angle, center)
+            hit.rotate(angle, center)
     
     def to_inch(self):
         if self.units == 'metric':
             super(ExcellonFileEx, self).to_inch()
             for hit in self.hits:
-                hit.position = (inch(hit.position[0]), inch(hit.position[1]))
+                hit.to_inch()
 
     def to_metric(self):
         if self.units == 'inch':
             super(ExcellonFileEx, self).to_metric()
             for hit in self.hits:
-                hit.position = (metric(hit.position[0]), metric(hit.position[1]))
+                hit.to_metric()
+    
+    def write(self, filename=None):
+        filename = filename if filename is not None else self.filename
+        with open(filename, 'w') as f:
+
+            for statement in self.statements:
+                if not isinstance(statement, ToolSelectionStmt):
+                    f.write(statement.to_excellon(self.settings) + '\n')
+                else:
+                    break
+
+            for tool in iter(self.tools.values()):
+                f.write(ToolSelectionStmt(
+                    tool.number).to_excellon(self.settings) + '\n')
+                for hit in self.hits:
+                    if hit.tool.number == tool.number:
+                        f.write(hit.to_excellon(self.settings) + '\n')
+            f.write(EndOfProgramStmt().to_excellon() + '\n')
+
+class DrillHitEx(DrillHit):
+    def rotate(self, angle, center=(0,0)):
+        self.position = rotate(*self.position, angle, center)
+
+    def to_excellon(self, settings):
+        return CoordinateStmtEx(*self.position).to_excellon(settings)
+
+class DrillSlotEx(DrillSlot):
+    def rotate(self, angle, center=(0,0)):
+        self.start = rotate(*self.start, angle, center)
+        self.end = rotate(*self.end, angle, center)
+
+    def to_excellon(self, settings):
+        return SlotStmt(*self.start, *self.end).to_excellon(settings)
+
+class DrillRout(object):
+    class Node(object):
+        def __init__(self, mode, x, y, radius):
+            self.mode = mode
+            self.position = (x, y)
+            self.radius = radius
+
+    @classmethod
+    def from_coordinates(cls, tool, coordinates):
+        nodes = [cls.Node(c.mode, c.x, c.y, c.radius) for c in coordinates]
+        return cls(tool, nodes)
+
+    def __init__(self, tool, nodes):
+        self.tool = tool
+        self.nodes = nodes
 
+    def to_excellon(self, settings):
+        node = self.nodes[0]
+        excellon = CoordinateStmtEx(*node.position, node.radius, 
+                                    CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n'
+        for node in self.nodes[1:]:
+            excellon += CoordinateStmtEx(*node.position, node.radius,
+                                        node.mode).to_excellon(settings) + '\n'
+        excellon += 'M16\nG05\n'
+        return excellon
+
+    def to_inch(self):
+        if self.tool.settings.units == 'metric':
+            self.tool.to_inch()
+            for node in self.nodes:
+                node.position = tuple(map(inch, node.position))
+                node.radius = inch(
+                    node.radius) if node.radius is not None else None
+
+    def to_metric(self):
+        if self.tool.settings.units == 'inch':
+            self.tool.to_metric()
+            for node in self.nodes:
+                node.position = tuple(map(metric, node.position))
+                node.radius = metric(
+                    node.radius) if node.radius is not None else None
+
+    def offset(self, x_offset=0, y_offset=0):
+        for node in self.nodes:
+            node.position = tuple(map(operator.add, node.position, (x_offset, y_offset)))
+
+    def rotate(self, angle, center=(0, 0)):
+        for node in self.nodes:
+            node.position = rotate(*node.position, angle, center)
 
 class UnitStmtEx(UnitStmt):
     @classmethod
@@ -62,3 +231,63 @@ class UnitStmtEx(UnitStmt):
                           'LZ' if self.zeros == 'leading' else 'TZ',
                           '0' * format[0], '0' * format[1])
         return stmt
+
+class CoordinateStmtEx(CoordinateStmt):
+    MODE_ROUT = 'ROUT'
+    MODE_LINEAR = 'LINEAR'
+    MODE_CIRCULER_CW = 'CW'
+    MODE_CIRCULER_CCW = 'CCW'
+
+    @classmethod
+    def from_statement(cls, stmt):
+        newStmt = cls(x=stmt.x, y=stmt.y)
+        newStmt.mode = stmt.mode
+        newStmt.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None
+        return newStmt
+
+    @classmethod
+    def from_excellon(cls, line, settings, **kwargs):
+        parts = line.split('A')
+        stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings))
+        if len(parts) > 1:
+            stmt.radius = parse_gerber_value(
+                parts[1], settings.format, settings.zero_suppression)
+        return stmt
+
+    def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs):
+        super(CoordinateStmtEx, self).__init__(x, y, **kwargs)
+        self.mode = mode
+        self.radius = radius
+    
+    def to_excellon(self, settings):
+        stmt = ''
+        if self.mode == self.MODE_ROUT:
+            stmt += "G00"
+        if self.mode == self.MODE_LINEAR:
+            stmt += "G01"
+        if self.mode == self.MODE_CIRCULER_CW:
+            stmt += "G02"
+        if self.mode == self.MODE_CIRCULER_CCW:
+            stmt += "G03"
+        if self.x is not None:
+            stmt += 'X%s' % write_gerber_value(self.x, settings.format,
+                                               settings.zero_suppression)
+        if self.y is not None:
+            stmt += 'Y%s' % write_gerber_value(self.y, settings.format,
+                                               settings.zero_suppression)
+        if self.radius is not None:
+            stmt += 'A%s' % write_gerber_value(self.radius, settings.format,
+                                               settings.zero_suppression)
+        return stmt
+
+    def __str__(self):
+        coord_str = ''
+        if self.x is not None:
+            coord_str += 'X: %g ' % self.x
+        if self.y is not None:
+            coord_str += 'Y: %g ' % self.y
+        if self.radius is not None:
+            coord_str += 'A: %g ' % self.radius
+
+        return '<Coordinate Statement: %s(%s)>' % \
+            (coord_str, self.mode if self.mode else 'HIT')
-- 
cgit 


From 02258202793e9c70f212171fe4a92f6bab3a6f72 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sun, 18 Aug 2019 14:16:22 +0900
Subject: fix a bug that unit traslation of excellon fail

---
 gerberex/excellon.py | 48 ++++++++++++++++++++++++++++++++----------------
 1 file changed, 32 insertions(+), 16 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 657f02a..04d10e9 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -124,13 +124,19 @@ class ExcellonFileEx(ExcellonFile):
     
     def to_inch(self):
         if self.units == 'metric':
-            super(ExcellonFileEx, self).to_inch()
+            for stmt in self.statements:
+                stmt.to_inch()
+            for tool in self.tools:
+                self.tools[tool].to_inch()
             for hit in self.hits:
                 hit.to_inch()
 
     def to_metric(self):
         if self.units == 'inch':
-            super(ExcellonFileEx, self).to_metric()
+            for stmt in self.statements:
+                stmt.to_metric()
+            for tool in self.tools:
+                self.tools[tool].to_metric()
             for hit in self.hits:
                 hit.to_metric()
     
@@ -153,13 +159,27 @@ class ExcellonFileEx(ExcellonFile):
             f.write(EndOfProgramStmt().to_excellon() + '\n')
 
 class DrillHitEx(DrillHit):
-    def rotate(self, angle, center=(0,0)):
+    def to_inch(self):
+        self.position = tuple(map(inch, self.position))
+
+    def to_metric(self):
+        self.position = tuple(map(metric, self.position))
+
+    def rotate(self, angle, center=(0, 0)):
         self.position = rotate(*self.position, angle, center)
 
     def to_excellon(self, settings):
         return CoordinateStmtEx(*self.position).to_excellon(settings)
 
 class DrillSlotEx(DrillSlot):
+    def to_inch(self):
+        self.start = tuple(map(inch, self.start))
+        self.end = tuple(map(inch, self.end))
+
+    def to_metric(self):
+        self.start = tuple(map(metric, self.start))
+        self.end = tuple(map(metric, self.end))
+
     def rotate(self, angle, center=(0,0)):
         self.start = rotate(*self.start, angle, center)
         self.end = rotate(*self.end, angle, center)
@@ -190,24 +210,20 @@ class DrillRout(object):
         for node in self.nodes[1:]:
             excellon += CoordinateStmtEx(*node.position, node.radius,
                                         node.mode).to_excellon(settings) + '\n'
-        excellon += 'M16\nG05\n'
+        excellon += 'M16\nG05'
         return excellon
 
     def to_inch(self):
-        if self.tool.settings.units == 'metric':
-            self.tool.to_inch()
-            for node in self.nodes:
-                node.position = tuple(map(inch, node.position))
-                node.radius = inch(
-                    node.radius) if node.radius is not None else None
+        for node in self.nodes:
+            node.position = tuple(map(inch, node.position))
+            node.radius = inch(
+                node.radius) if node.radius is not None else None
 
     def to_metric(self):
-        if self.tool.settings.units == 'inch':
-            self.tool.to_metric()
-            for node in self.nodes:
-                node.position = tuple(map(metric, node.position))
-                node.radius = metric(
-                    node.radius) if node.radius is not None else None
+        for node in self.nodes:
+            node.position = tuple(map(metric, node.position))
+            node.radius = metric(
+                node.radius) if node.radius is not None else None
 
     def offset(self, x_offset=0, y_offset=0):
         for node in self.nodes:
-- 
cgit 


From f8fe16708502e0588d9d0be42d097a933d6f12d3 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sun, 18 Aug 2019 18:33:41 +0900
Subject: zero supressing in aperture macro definition

---
 gerberex/am_expression.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'gerberex')

diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py
index 4bb1d78..5400130 100644
--- a/gerberex/am_expression.py
+++ b/gerberex/am_expression.py
@@ -49,7 +49,8 @@ class AMConstantExpression(AMExpression):
         return self
     
     def to_gerber(self, settings=None):
-        return '%.6f' % self._value
+        gerber = '%.6g' % self._value
+        return '%.6f' % self._value if 'e' in gerber else gerber
 
     def to_instructions(self):
         return [(OpCode.PUSH, self._value)]
-- 
cgit 


From 36956f93fe1773229b6b7bb1b851adf169f97f79 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sun, 25 Aug 2019 12:39:01 +0900
Subject: improve routing mode compatibility with excellon specification

---
 gerberex/excellon.py | 242 +++++++++++++++++++++++++++++++++++----------------
 1 file changed, 166 insertions(+), 76 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 04d10e9..7014a6e 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -5,8 +5,10 @@
 
 import operator
 
+import gerber.excellon
 from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot
-from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \
+from gerber.excellon_statements import ExcellonStatement, UnitStmt, CoordinateStmt, UnknownStmt, \
+                                       SlotStmt, DrillModeStmt, RouteModeStmt, LinearModeStmt, \
                                        ToolSelectionStmt, ZAxisRoutPositionStmt, \
                                        RetractWithClampingStmt, RetractWithoutClampingStmt, \
                                        EndOfProgramStmt
@@ -19,6 +21,8 @@ def loads(data, filename=None, settings=None, tools=None, format=None):
         settings = FileSettings(**detect_excellon_format(data))
         if format:
             settings.format = format
+    gerber.excellon.CoordinateStmt = CoordinateStmtEx
+    gerber.excellon.UnitStmt = UnitStmtEx
     file = ExcellonParser(settings, tools).parse_raw(data, filename)
     return ExcellonFileEx.from_file(file)
 
@@ -26,49 +30,78 @@ class ExcellonFileEx(ExcellonFile):
     @classmethod
     def from_file(cls, file):
         def correct_statements():
-            lazy_stmt = None
-            modifier = None
             for stmt in file.statements:
-                modifier = lazy_stmt
-                lazy_stmt = None
-                if isinstance(stmt, UnitStmt):
-                    yield UnitStmtEx.from_statement(stmt)
-                elif isinstance(stmt, CoordinateStmt):
-                    new_stmt = CoordinateStmtEx.from_statement(stmt)
-                    if modifier and new_stmt.mode is None:
-                        new_stmt.mode = modifier
-                    yield new_stmt
-                elif isinstance(stmt, UnknownStmt):
+                if isinstance(stmt, UnknownStmt):
                     line = stmt.stmt.strip()
-                    mode = None
                     if line[:3] == 'G02':
-                        mode = CoordinateStmtEx.MODE_CIRCULER_CW
+                        yield CircularCWModeStmt()
+                        if len(line) > 3:
+                            yield CoordinateStmtEx.from_excellon(line[3:], file.settings)
                     elif line[:3] == 'G03':
-                        mode = CoordinateStmtEx.MODE_CIRCULER_CCW
+                        yield CircularCCWModeStmt()
+                        if len(line) > 3:
+                            yield CoordinateStmtEx.from_excellon(line[3:], file.settings)
+                    elif line[0] == 'X' or line[0] == 'Y' or line[0] == 'A' or line[0] == 'I':
+                        yield CoordinateStmtEx.from_excellon(line, file.settings)
                     else:
                         yield stmt
-                        continue
-                    if len(line) == 3:
-                        lazy_stmt = mode
-                        continue
-                    new_stmt = CoordinateStmtEx.from_excellon(line[3:], file.settings)
-                    new_stmt.mode = mode
-                    yield new_stmt
                 else:
                     yield stmt
 
         def generate_hits(statements):
+            class CoordinateCtx:
+                def __init__(self, notation):
+                    self.notation = notation
+                    self.x = 0.
+                    self.y = 0.
+                    self.radius = None
+                    self.center_offset = None
+                
+                def update(self, x=None, y=None, radius=None, center_offset=None):
+                    if self.notation == 'absolute':
+                        if x is not None:
+                            self.x = x
+                        if y is not None:
+                            self.y = y
+                    else:
+                        if x is not None:
+                            self.x += x
+                        if y is not None:
+                            self.y += y
+                    if radius is not None:
+                        self.radius = radius
+                    if center_offset is not None:
+                        self.center_offset = center_offset
+                
+                def node(self, mode, center_offset):
+                    radius, offset = None, None
+                    if mode == DrillRout.MODE_CIRCULER_CW or mode == DrillRout.MODE_CIRCULER_CCW:
+                        if center_offset is None:
+                            radius = self.radius
+                            offset = self.center_offset
+                        else:
+                            radius = None
+                            offset = center_offset
+                    return DrillRout.Node(mode, self.x, self.y, radius, offset)
+
             STAT_DRILL = 0
             STAT_ROUT_UP = 1
             STAT_ROUT_DOWN = 2
+
             status = STAT_DRILL
             current_tool = None
-            rout_statements = []
+            rout_mode = None
+            coordinate_ctx = CoordinateCtx(file.notation)
+            rout_nodes = []
+
+            last_position = (0., 0.)
+            last_radius = None
+            last_center_offset = None
 
-            def make_rout(status, statements):
-                if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None:
+            def make_rout(status, nodes):
+                if status != STAT_ROUT_DOWN or len(nodes) == 0 or current_tool is None:
                     return None
-                return DrillRout.from_coordinates(current_tool, statements)
+                return DrillRout(current_tool, nodes)
 
             for stmt in statements:
                 if isinstance(stmt, ToolSelectionStmt):
@@ -79,31 +112,48 @@ class ExcellonFileEx(ExcellonFile):
                     if rout is not None:
                         yield rout
                     status = STAT_DRILL
+                    rout_mode = None
+                elif isinstance(stmt, RouteModeStmt):
+                    if status == STAT_DRILL:
+                        status = STAT_ROUT_UP
+                        rout_mode = DrillRout.MODE_ROUT
+                    else:
+                        rout_mode = DrillRout.MODE_LINEAR
+
+                elif isinstance(stmt, LinearModeStmt):
+                    rout_mode = DrillRout.MODE_LINEAR
+                elif isinstance(stmt, CircularCWModeStmt):
+                    rout_mode = DrillRout.MODE_CIRCULER_CW
+                elif isinstance(stmt, CircularCCWModeStmt):
+                    rout_mode = DrillRout.MODE_CIRCULER_CCW
                 elif isinstance(stmt, ZAxisRoutPositionStmt) and status == STAT_ROUT_UP:
                     status = STAT_ROUT_DOWN
                 elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt):
-                    rout = make_rout(status, rout_statements)
+                    rout = make_rout(status, rout_nodes)
                     rout_statements = []
                     if rout is not None:
                         yield rout
                     status = STAT_ROUT_UP
                 elif isinstance(stmt, SlotStmt):
-                    yield DrillSlotEx(current_tool, (stmt.x_start, stmt.y_start), 
-                                      (stmt.x_end, stmt.y_end), DrillSlotEx.TYPE_G85)
+                    coordinate_ctx.update(stmt.x_start, stmt.y_start)
+                    x_start = coordinate_ctx.x
+                    y_start = coordinate_ctx.y
+                    coordinate_ctx.update(stmt.x_end, stmt.y_end)
+                    x_end = coordinate_ctx.x
+                    y_end = coordinate_ctx.y
+                    yield DrillSlotEx(current_tool, (x_start, y_start), 
+                                      (x_end, y_end), DrillSlotEx.TYPE_G85)
                 elif isinstance(stmt, CoordinateStmtEx):
-                    if stmt.mode is None:
-                        if status != STAT_DRILL:
-                            raise Exception('invalid statement sequence')
-                        yield DrillHitEx(current_tool, (stmt.x, stmt.y))
-                    else:
-                        if stmt.mode == stmt.MODE_ROUT:
-                            status = STAT_ROUT_UP
-                        if status == STAT_ROUT_UP:
-                            rout_statements = [stmt]
+                    center_offset = (stmt.i, stmt.j) \
+                                    if stmt.i is not None and stmt.j is not None else None
+                    coordinate_ctx.update(stmt.x, stmt.y, stmt.radius, center_offset)
+                    if stmt.x is not None or stmt.y is not None:
+                        if status == STAT_DRILL:
+                            yield DrillHitEx(current_tool, (coordinate_ctx.x, coordinate_ctx.y))
+                        elif status == STAT_ROUT_UP:
+                            rout_nodes = [coordinate_ctx.node(DrillRout.MODE_ROUT, None)]
                         elif status == STAT_ROUT_DOWN:
-                            rout_statements.append(stmt)
-                        else:
-                            raise Exception('invalid statement sequence')
+                            rout_nodes.append(coordinate_ctx.node(rout_mode, center_offset))
 
         statements = [s for s in correct_statements()]
         hits = [h for h in generate_hits(statements)]
@@ -188,28 +238,33 @@ class DrillSlotEx(DrillSlot):
         return SlotStmt(*self.start, *self.end).to_excellon(settings)
 
 class DrillRout(object):
+    MODE_ROUT = 'G00'
+    MODE_LINEAR = 'G01'
+    MODE_CIRCULER_CW = 'G02'
+    MODE_CIRCULER_CCW = 'G03'
+
     class Node(object):
-        def __init__(self, mode, x, y, radius):
+        def __init__(self, mode, x, y, radius=None, center_offset=None):
             self.mode = mode
             self.position = (x, y)
             self.radius = radius
+            self.center_offset = center_offset
 
-    @classmethod
-    def from_coordinates(cls, tool, coordinates):
-        nodes = [cls.Node(c.mode, c.x, c.y, c.radius) for c in coordinates]
-        return cls(tool, nodes)
+        def to_excellon(self, settings):
+            center_offset = self.center_offset \
+                            if self.center_offset is not None else (None, None)
+            return self.mode + CoordinateStmtEx(
+                *self.position, self.radius, *center_offset).to_excellon(settings)
 
     def __init__(self, tool, nodes):
         self.tool = tool
         self.nodes = nodes
+        self.nodes[0].mode = self.MODE_ROUT
 
     def to_excellon(self, settings):
-        node = self.nodes[0]
-        excellon = CoordinateStmtEx(*node.position, node.radius, 
-                                    CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n'
+        excellon = self.nodes[0].to_excellon(settings) + '\nM15\n'
         for node in self.nodes[1:]:
-            excellon += CoordinateStmtEx(*node.position, node.radius,
-                                        node.mode).to_excellon(settings) + '\n'
+            excellon += node.to_excellon(settings) + '\n'
         excellon += 'M16\nG05'
         return excellon
 
@@ -218,12 +273,16 @@ class DrillRout(object):
             node.position = tuple(map(inch, node.position))
             node.radius = inch(
                 node.radius) if node.radius is not None else None
+            if node.center_offset is not None:
+                node.center_offset = tuple(map(inch, node.center_offset))
 
     def to_metric(self):
         for node in self.nodes:
             node.position = tuple(map(metric, node.position))
             node.radius = metric(
                 node.radius) if node.radius is not None else None
+            if node.center_offset is not None:
+                node.center_offset = tuple(map(metric, node.center_offset))
 
     def offset(self, x_offset=0, y_offset=0):
         for node in self.nodes:
@@ -232,6 +291,8 @@ class DrillRout(object):
     def rotate(self, angle, center=(0, 0)):
         for node in self.nodes:
             node.position = rotate(*node.position, angle, center)
+            if node.center_offset is not None:
+                node.center_offset = rotate(*node.center_offset, angle, (0., 0.))
 
 class UnitStmtEx(UnitStmt):
     @classmethod
@@ -243,48 +304,69 @@ class UnitStmtEx(UnitStmt):
     
     def to_excellon(self, settings=None):
         format = settings.format if settings else self.format
-        stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
-                          'LZ' if self.zeros == 'leading' else 'TZ',
-                          '0' * format[0], '0' * format[1])
+        stmt = None
+        if self.units == 'inch' and format == (2, 4):
+            stmt = 'INCH,%s' % ('LZ' if self.zeros == 'leading' else 'TZ')
+        else:
+            stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
+                              'LZ' if self.zeros == 'leading' else 'TZ',
+                              '0' * format[0], '0' * format[1])
         return stmt
 
-class CoordinateStmtEx(CoordinateStmt):
-    MODE_ROUT = 'ROUT'
-    MODE_LINEAR = 'LINEAR'
-    MODE_CIRCULER_CW = 'CW'
-    MODE_CIRCULER_CCW = 'CCW'
+class CircularCWModeStmt(ExcellonStatement):
+
+    def __init__(self, **kwargs):
+        super(CircularCWModeStmt, self).__init__(**kwargs)
+
+    def to_excellon(self, settings=None):
+        return 'G02'
 
+class CircularCCWModeStmt(ExcellonStatement):
+
+    def __init__(self, **kwargs):
+        super(CircularCCWModeStmt, self).__init__(**kwargs)
+
+    def to_excellon(self, settings=None):
+        return 'G02'
+
+class CoordinateStmtEx(CoordinateStmt):
     @classmethod
     def from_statement(cls, stmt):
         newStmt = cls(x=stmt.x, y=stmt.y)
-        newStmt.mode = stmt.mode
         newStmt.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None
         return newStmt
 
     @classmethod
     def from_excellon(cls, line, settings, **kwargs):
-        parts = line.split('A')
-        stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings))
-        if len(parts) > 1:
+        stmt = None
+        if 'A' in line:
+            parts = line.split('A')
+            stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) \
+                   if parts[0] != '' else cls()
             stmt.radius = parse_gerber_value(
                 parts[1], settings.format, settings.zero_suppression)
+        elif 'I' in line:
+            jparts = line.split('J')
+            iparts = jparts[0].split('I')
+            stmt = cls.from_statement(CoordinateStmt.from_excellon(iparts[0], settings)) \
+                   if iparts[0] != '' else cls()
+            stmt.i = parse_gerber_value(
+                iparts[1], settings.format, settings.zero_suppression)
+            stmt.j = parse_gerber_value(
+                jparts[1], settings.format, settings.zero_suppression)
+        else:
+            stmt = cls.from_statement(CoordinateStmt.from_excellon(line, settings))
+            
         return stmt
 
-    def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs):
+    def __init__(self, x=None, y=None, radius=None, i=None, j=None, **kwargs):
         super(CoordinateStmtEx, self).__init__(x, y, **kwargs)
-        self.mode = mode
         self.radius = radius
+        self.i = i
+        self.j = j
     
     def to_excellon(self, settings):
         stmt = ''
-        if self.mode == self.MODE_ROUT:
-            stmt += "G00"
-        if self.mode == self.MODE_LINEAR:
-            stmt += "G01"
-        if self.mode == self.MODE_CIRCULER_CW:
-            stmt += "G02"
-        if self.mode == self.MODE_CIRCULER_CCW:
-            stmt += "G03"
         if self.x is not None:
             stmt += 'X%s' % write_gerber_value(self.x, settings.format,
                                                settings.zero_suppression)
@@ -294,6 +376,11 @@ class CoordinateStmtEx(CoordinateStmt):
         if self.radius is not None:
             stmt += 'A%s' % write_gerber_value(self.radius, settings.format,
                                                settings.zero_suppression)
+        elif self.i is not None and self.j is not None:
+            stmt += 'I%sJ%s' % (write_gerber_value(self.i, settings.format,
+                                                   settings.zero_suppression),
+                                write_gerber_value(self.j, settings.format,
+                                                   settings.zero_suppression))
         return stmt
 
     def __str__(self):
@@ -304,6 +391,9 @@ class CoordinateStmtEx(CoordinateStmt):
             coord_str += 'Y: %g ' % self.y
         if self.radius is not None:
             coord_str += 'A: %g ' % self.radius
+        if self.i is not None:
+            coord_str += 'I: %g ' % self.i
+        if self.j is not None:
+            coord_str += 'J: %g ' % self.j
 
-        return '<Coordinate Statement: %s(%s)>' % \
-            (coord_str, self.mode if self.mode else 'HIT')
+        return '<Coordinate Statement: %s>' % (coord_str)
-- 
cgit 


From 13ab9db6e7571f3d0fcb406bfe6795eea9ce4e1c Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sun, 25 Aug 2019 20:16:53 +0900
Subject: support incremental coordinate for excellon

---
 gerberex/composition.py | 27 +--------------------------
 gerberex/dxf.py         | 31 +++----------------------------
 gerberex/excellon.py    | 17 ++++++++++-------
 3 files changed, 14 insertions(+), 61 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/composition.py b/gerberex/composition.py
index 7f691f5..7b1548e 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -124,8 +124,6 @@ class GerberComposition(Composition):
 class DrillComposition(Composition):
     def __init__(self, settings=None, comments=None):
         super(DrillComposition, self).__init__(settings, comments)
-        self.header1_statements = []
-        self.header2_statements = []
         self.tools = []
         self.hits = []
         self.dxf_statements = []
@@ -140,12 +138,6 @@ class DrillComposition(Composition):
 
     def dump(self, path):
         def statements():
-            for s in self.header1_statements:
-                yield s.to_excellon(self.settings)
-            for t in self.tools:
-                yield t.to_excellon(self.settings)
-            for s in self.header2_statements:
-                yield s.to_excellon(self.settings)
             for t in self.tools:
                 yield ToolSelectionStmt(t.number).to_excellon(self.settings)
                 for h in self.hits:
@@ -157,6 +149,7 @@ class DrillComposition(Composition):
             yield EndOfProgramStmt().to_excellon()
         
         with open(path, 'w') as f:
+            gerberex.excellon.write_excellon_header(f, self.settings, self.tools)
             for statement in statements():
                 f.write(statement + '\n')
 
@@ -171,20 +164,6 @@ class DrillComposition(Composition):
             else:
                 file.to_inch()
 
-        if not self.header1_statements:
-            in_header1 = True
-            for statement in file.statements:
-                if not isinstance(statement, ToolSelectionStmt):
-                    if isinstance(statement, ExcellonTool):
-                        in_header1 = False
-                    else:
-                        if in_header1:
-                            self.header1_statements.append(statement)
-                        else:
-                            self.header2_statements.append(statement)
-                else:
-                    break
-        
         for tool in iter(file.tools.values()):
             num = tool.number
             tool_map[num] = self._register_tool(tool)
@@ -202,10 +181,6 @@ class DrillComposition(Composition):
             else:
                 file.to_inch()
 
-        if not self.header1_statements:
-            self.header1_statements = [file.header]
-            self.header2_statements = [file.header2]
-        
         tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width))
         self.dxf_statements.append((tool.number, file.statements))
 
diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index ba02f08..389cca9 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -13,6 +13,7 @@ from gerber.excellon_statements import ExcellonTool
 from gerber.excellon_statements import CoordinateStmt
 from gerberex.utility import is_equal_point, is_equal_value
 from gerberex.dxf_path import generate_closed_paths
+from gerberex.excellon import write_excellon_header
 
 ACCEPTABLE_ERROR = 0.001
 
@@ -465,31 +466,8 @@ class DxfHeaderStatement(object):
         )
     
     def to_excellon(self, settings):
-        return 'M48\n'\
-               'FMAT,2\n'\
-               'ICI,{0}\n'\
-               '{1},{2},{3}.{4}\n'\
-               '{5}'.format(
-            'ON' if settings.notation == 'incremental' else 'OFF',
-            'INCH' if settings.units == 'inch' else 'METRIC',
-            'TZ' if settings.zero_suppression == 'leading' else 'LZ',
-            '0' * settings.format[0], '0' * settings.format[1],
-            'M72' if settings.units == 'inch' else 'M71'
-        )
-
-    def to_inch(self):
-        pass
-
-    def to_metric(self):
         pass
 
-class DxfHeader2Statement(object):
-    def to_gerber(self, settings):
-        pass
-
-    def to_excellon(self, settings):
-        return '%'
-
     def to_inch(self):
         pass
 
@@ -555,7 +533,6 @@ class DxfFile(CamFile):
         self._draw_mode = draw_mode
         self.header = DxfHeaderStatement()
         
-        self.header2 = DxfHeader2Statement()
         self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
         self.statements = DxfStatements(
             statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
@@ -607,10 +584,8 @@ class DxfFile(CamFile):
                 f.write(self.statements.to_gerber(self.settings) + '\n')
                 f.write('M02*\n')
             else:
-                tool = ExcellonTool(self.settings, number=1, diameter=self.width)
-                f.write(self.header.to_excellon(self.settings) + '\n')
-                f.write(tool.to_excellon(self.settings) + '\n')
-                f.write(self.header2.to_excellon(self.settings) + '\n')
+                tools = [ExcellonTool(self.settings, number=1, diameter=self.width)]
+                write_excellon_header(f, self.settings, tools)
                 f.write('T01\n')
                 f.write(self.statements.to_excellon(self.settings) + '\n')
                 f.write('M30\n')
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 7014a6e..a8c01c7 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -26,6 +26,13 @@ def loads(data, filename=None, settings=None, tools=None, format=None):
     file = ExcellonParser(settings, tools).parse_raw(data, filename)
     return ExcellonFileEx.from_file(file)
 
+def write_excellon_header(file, settings, tools):
+    file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % 
+               UnitStmtEx(settings.units, settings.zeros, settings.format).to_excellon(settings))
+    for tool in tools:
+        file.write(tool.to_excellon(settings) + '\n')
+    file.write('%%\nG90\n%s\n' % ('M72' if settings.units == 'inch' else 'M71'))
+
 class ExcellonFileEx(ExcellonFile):
     @classmethod
     def from_file(cls, file):
@@ -180,6 +187,7 @@ class ExcellonFileEx(ExcellonFile):
                 self.tools[tool].to_inch()
             for hit in self.hits:
                 hit.to_inch()
+            self.units = 'inch'
 
     def to_metric(self):
         if self.units == 'inch':
@@ -189,17 +197,12 @@ class ExcellonFileEx(ExcellonFile):
                 self.tools[tool].to_metric()
             for hit in self.hits:
                 hit.to_metric()
+            self.units = 'metric'
     
     def write(self, filename=None):
         filename = filename if filename is not None else self.filename
         with open(filename, 'w') as f:
-
-            for statement in self.statements:
-                if not isinstance(statement, ToolSelectionStmt):
-                    f.write(statement.to_excellon(self.settings) + '\n')
-                else:
-                    break
-
+            write_excellon_header(f, self.settings, [self.tools[t] for t in self.tools])
             for tool in iter(self.tools.values()):
                 f.write(ToolSelectionStmt(
                     tool.number).to_excellon(self.settings) + '\n')
-- 
cgit 


From 2b1c751ff76ebd6901633235ee694cc93dabce81 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Mon, 9 Sep 2019 09:07:38 +0900
Subject: improve compatibility with RS-274x specification: - can merge
 multiple files having different file scope modifier, such as AS, MI, OF, SF,
 and IR - support modal coordinate notation

---
 gerberex/common.py      |   2 +-
 gerberex/composition.py |  53 +++++------
 gerberex/dxf.py         |  31 +------
 gerberex/excellon.py    |   2 +-
 gerberex/rs274x.py      | 241 +++++++++++++++++++++++++++++++++++++++++-------
 5 files changed, 238 insertions(+), 91 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/common.py b/gerberex/common.py
index 47bc6b3..6e8a832 100644
--- a/gerberex/common.py
+++ b/gerberex/common.py
@@ -25,7 +25,7 @@ def loads(data, filename=None, format=None):
 
     fmt = detect_file_format(data)
     if fmt == 'rs274x':
-        file = gerber.rs274x.loads(data, filename=filename)
+        file = gerberex.rs274x.loads(data, filename=filename)
         return gerberex.rs274x.GerberFile.from_gerber_file(file)
     elif fmt == 'excellon':
         return gerberex.excellon.loads(data, filename=filename, format=format)
diff --git a/gerberex/composition.py b/gerberex/composition.py
index 7b1548e..634640e 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -22,7 +22,6 @@ class GerberComposition(Composition):
 
     def __init__(self, settings=None, comments=None):
         super(GerberComposition, self).__init__(settings, comments)
-        self.param_statements = []
         self.aperture_macros = {}
         self.apertures = []
         self.drawings = []
@@ -37,8 +36,6 @@ class GerberComposition(Composition):
 
     def dump(self, path):
         def statements():
-            for s in self.param_statements:
-                yield s
             for k in self.aperture_macros:
                 yield self.aperture_macros[k]
             for s in self.apertures:
@@ -46,12 +43,14 @@ class GerberComposition(Composition):
             for s in self.drawings:
                 yield s
             yield EofStmt()
+        self.settings.notation = 'absolute'
+        self.settings.zeros = 'trailing'
         with open(path, 'w') as f:
+            gerberex.rs274x.write_gerber_header(f, self.settings)
             for statement in statements():
                 f.write(statement.to_gerber(self.settings) + '\n')
 
     def _merge_gerber(self, file):
-        param_statements = []
         aperture_macro_map = {}
         aperture_map = {}
 
@@ -61,34 +60,27 @@ class GerberComposition(Composition):
             else:
                 file.to_inch()
 
-        for statement in file.statements:
-            if statement.type == 'COMMENT':
-                self.comments.append(statement.comment)
-            elif statement.type == 'PARAM':
-                if statement.param == 'AM':
-                    name = statement.name
-                    newname = self._register_aperture_macro(statement)
-                    aperture_macro_map[name] = newname
-                elif statement.param == 'AD':
-                    if not statement.shape in ['C', 'R', 'O']:
-                        statement.shape = aperture_macro_map[statement.shape]
-                    dnum = statement.d
-                    newdnum = self._register_aperture(statement)
-                    aperture_map[dnum] = newdnum
-                elif statement.param == 'LP':
-                    self.drawings.append(statement)
-                else:
-                    param_statements.append(statement)
-            elif statement.type in ['EOF', "DEPRECATED"]:
-                pass
-            else:
-                if statement.type == 'APERTURE':
-                    statement.d = aperture_map[statement.d]
-                self.drawings.append(statement)
+        for macro in file.aperture_macros:
+            statement = file.aperture_macros[macro]
+            name = statement.name
+            newname = self._register_aperture_macro(statement)
+            aperture_macro_map[name] = newname
+
+        for statement in file.aperture_defs:
+            if statement.param == 'AD':
+                if statement.shape in aperture_macro_map:
+                    statement.shape = aperture_macro_map[statement.shape]
+                dnum = statement.d
+                newdnum = self._register_aperture(statement)
+                aperture_map[dnum] = newdnum
+
+        for statement in file.main_statements:
+            if statement.type == 'APERTURE':
+                statement.d = aperture_map[statement.d]
+            self.drawings.append(statement)
         
         if not self.settings:
-            self.settings = file.settings
-            self.param_statements = param_statements
+            self.settings = file.context
 
     def _merge_dxf(self, file):
         if self.settings:
@@ -102,7 +94,6 @@ class GerberComposition(Composition):
 
         if not self.settings:
             self.settings = file.settings
-            self.param_statements = [file.header]
 
 
     def _register_aperture_macro(self, statement):
diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 389cca9..ae543ae 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -14,6 +14,7 @@ from gerber.excellon_statements import CoordinateStmt
 from gerberex.utility import is_equal_point, is_equal_value
 from gerberex.dxf_path import generate_closed_paths
 from gerberex.excellon import write_excellon_header
+from gerberex.rs274x import write_gerber_header
 
 ACCEPTABLE_ERROR = 0.001
 
@@ -395,6 +396,8 @@ class DxfStatements(object):
 
     def to_gerber(self, settings=FileSettings()):
         def gerbers():
+            yield 'G75*'
+            yield '%LPD*%'
             yield 'D{0}*'.format(self.dcode)
             if self.draw_mode == DxfFile.DM_FILL:
                 yield 'G36*'
@@ -451,29 +454,6 @@ class DxfStatements(object):
         for statement in self.statements:
             statement.rotate(angle, center)
 
-class DxfHeaderStatement(object):
-    def to_gerber(self, settings):
-        return 'G75*\n'\
-               '%MO{0}*%\n'\
-               '%OFA0B0*%\n'\
-               '%FS{1}AX{2}{3}Y{4}{5}*%\n'\
-               '%IPPOS*%\n'\
-               '%LPD*%'.format(
-            'IN' if settings.units == 'inch' else 'MM',
-            'L' if settings.zero_suppression == 'leading' else 'T',
-            settings.format[0], settings.format[1],
-            settings.format[0], settings.format[1]
-        )
-    
-    def to_excellon(self, settings):
-        pass
-
-    def to_inch(self):
-        pass
-
-    def to_metric(self):
-        pass
-
 class DxfFile(CamFile):
     DM_LINE = 0
     DM_FILL = 1
@@ -531,7 +511,6 @@ class DxfFile(CamFile):
 
         super(DxfFile, self).__init__(settings=settings, filename=filename)
         self._draw_mode = draw_mode
-        self.header = DxfHeaderStatement()
         
         self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
         self.statements = DxfStatements(
@@ -579,7 +558,7 @@ class DxfFile(CamFile):
         filename = filename if filename is not None else self.filename
         with open(filename, 'w') as f:
             if filetype == self.FT_RX274X:
-                f.write(self.header.to_gerber(self.settings) + '\n')
+                write_gerber_header(f, self.settings)
                 f.write(self.aperture.to_gerber(self.settings) + '\n')
                 f.write(self.statements.to_gerber(self.settings) + '\n')
                 f.write('M02*\n')
@@ -593,7 +572,6 @@ class DxfFile(CamFile):
 
     def to_inch(self):
         if self.units == 'metric':
-            self.header.to_inch()
             self.aperture.to_inch()
             self.statements.to_inch()
             self.pitch = inch(self.pitch)
@@ -601,7 +579,6 @@ class DxfFile(CamFile):
 
     def to_metric(self):
         if self.units == 'inch':
-            self.header.to_metric()
             self.aperture.to_metric()
             self.statements.to_metric()
             self.pitch = metric(self.pitch)
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index a8c01c7..4f867be 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -27,7 +27,7 @@ def loads(data, filename=None, settings=None, tools=None, format=None):
     return ExcellonFileEx.from_file(file)
 
 def write_excellon_header(file, settings, tools):
-    file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % 
+    file.write('M48\nFMAT,2\nICI,OFF\n%s\n' %
                UnitStmtEx(settings.units, settings.zeros, settings.format).to_excellon(settings))
     for tool in tools:
         file.write(tool.to_excellon(settings) + '\n')
diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
index 13d3421..fae0b32 100644
--- a/gerberex/rs274x.py
+++ b/gerberex/rs274x.py
@@ -3,10 +3,29 @@
 
 # Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
 
+from gerber.cam import FileSettings
 import gerber.rs274x
-from gerber.gerber_statements import ADParamStmt, CoordStmt
+from gerber.gerber_statements import *
 from gerberex.gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx
 from gerberex.utility import rotate
+import re
+
+def loads(data, filename=None):
+    cls = gerber.rs274x.GerberParser
+    cls.SF = \
+        r"(?P<param>SF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=cls.DECIMAL)
+    cls.PARAMS = (cls.FS, cls.MO, cls.LP, cls.AD_CIRCLE, 
+                  cls.AD_RECT, cls.AD_OBROUND, cls.AD_POLY,
+                  cls.AD_MACRO, cls.AM, cls.AS, cls.IF, cls.IN, 
+                  cls.IP, cls.IR, cls.MI, cls.OF, cls.SF, cls.LN)
+    cls.PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in cls.PARAMS]
+    return cls().parse_raw(data, filename)
+
+def write_gerber_header(file, settings):
+    file.write('%s\n%s\n%%IPPOS*%%\n' % (
+               MOParamStmt('MO', settings.units).to_gerber(settings),
+               FSParamStmt('FS', settings.zero_suppression, 
+                           settings.notation, settings.format).to_gerber(settings)))
 
 class GerberFile(gerber.rs274x.GerberFile):
     @classmethod
@@ -14,29 +33,74 @@ class GerberFile(gerber.rs274x.GerberFile):
         if not isinstance(gerber_file, gerber.rs274x.GerberFile):
             raise Exception('only gerber.rs274x.GerberFile object is specified')
         
-        def swap_statement(statement):
-            if isinstance(statement, AMParamStmt) and not isinstance(statement, AMParamStmtEx):
-                return AMParamStmtEx.from_stmt(statement)
-            elif isinstance(statement, ADParamStmt) and not isinstance(statement, AMParamStmtEx):
-                return ADParamStmtEx.from_stmt(statement)
-            else:
-                return statement
-        statements = [swap_statement(statement) for statement in gerber_file.statements]
-        return cls(statements, gerber_file.settings, gerber_file.primitives,\
+        return cls(gerber_file.statements, gerber_file.settings, gerber_file.primitives,\
                    gerber_file.apertures, gerber_file.filename)
 
     def __init__(self, statements, settings, primitives, apertures, filename=None):
         super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename)
+        self.context = GerberContext.from_settings(self.settings)
+        self.aperture_macros = {}
+        self.aperture_defs = []
+        self.main_statements = []
+        for stmt in self.statements:
+            type, stmts = self.context.normalize_statement(stmt)
+            if type == self.context.TYPE_AM:
+                for mdef in stmts:
+                    self.aperture_macros[mdef.name] = mdef
+            elif type == self.context.TYPE_AD:
+                self.aperture_defs.extend(stmts)
+            elif type == self.context.TYPE_MAIN:
+                self.main_statements.extend(stmts)
+        if self.context.angle != 0:
+            self.rotate(self.context.angle)
+        self.context.notation = 'absolute'
+        self.context.zeros = 'trailing'
+
+    def write(self, filename=None):
+        self.context.notation = 'absolute'
+        self.context.zeros = 'trailing'
+        self.context.format = self.format
+        self.units = self.units
+        filename=filename if filename is not None else self.filename
+        with open(filename, 'w') as f:
+            write_gerber_header(f, self.context)
+            for macro in self.aperture_macros:
+                f.write(self.aperture_macros[macro].to_gerber(self.context) + '\n')
+            for aperture in self.aperture_defs:
+                f.write(aperture.to_gerber(self.context) + '\n')
+            for statement in self.main_statements:
+                f.write(statement.to_gerber(self.context) + '\n')
+            f.write('M02*\n')
+
+    def to_inch(self):
+        if self.units == 'metric':
+            for macro in self.aperture_macros:
+                self.aperture_macros[macro].to_inch()
+            for aperture in self.aperture_defs:
+                aperture.to_inch()
+            for statement in self.statements:
+                statement.to_inch()
+            self.units = 'inch'
+            self.context.units = 'inch'
+
+    def to_metric(self):
+        if self.units == 'inch':
+            for macro in self.aperture_macros:
+                self.aperture_macros[macro].to_metric()
+            for aperture in self.aperture_defs:
+                aperture.to_metric()
+            for statement in self.statements:
+                statement.to_metric()
+            self.units='metric'
+            self.context.units='metric'
 
     def offset(self, x_offset=0, y_offset=0):
-        for statement in self.statements:
+        for statement in self.main_statements:
             if isinstance(statement, CoordStmt):
                 if statement.x is not None:
                     statement.x += x_offset
                 if statement.y is not None:
                     statement.y += y_offset
-            else:
-                statement.offset(x_offset, y_offset)
         for primitive in self.primitives:
             primitive.offset(x_offset, y_offset)
 
@@ -48,10 +112,10 @@ class GerberFile(gerber.rs274x.GerberFile):
         last_y = 0
         last_rx = 0
         last_ry = 0
-        for statement in self.statements:
-            if isinstance(statement, AMParamStmtEx):
-                statement.rotate(angle, center)
-            elif isinstance(statement, CoordStmt) and statement.x != None and statement.y != None:
+        for name in self.aperture_macros:
+            self.aperture_macros[name].rotate(angle, center)
+        for statement in self.main_statements:
+            if isinstance(statement, CoordStmt) and statement.x != None and statement.y != None:
                 if statement.i != None and statement.j != None:
                     cx = last_x + statement.i
                     cy = last_y + statement.j
@@ -77,31 +141,21 @@ class GerberFile(gerber.rs274x.GerberFile):
         ]
 
         need_to_change = False
-        insert_point = 0
-        last_aperture = 0
-        macros = {}
-        for idx in range(0, len(self.statements)):
-            statement = self.statements[idx]
-            if isinstance(statement, AMParamStmtEx):
-                macros[statement.name] = statement
-                if not need_to_change:
-                    insert_point = idx + 1
+        for statement in self.aperture_defs:
             if isinstance(statement, ADParamStmt) and statement.shape in ['R', 'O', 'P']:
                 need_to_change = True
-                last_aperture = idx
         
         if need_to_change:
             for idx in range(0, len(macro_defs)):
                 macro_def = macro_defs[idx]
                 name = macro_def[0]
                 num = 1
-                while name in macros:
+                while name in self.aperture_macros:
                     name = '%s_%d' % (macro_def[0], num)
                     num += 1
-                self.statements.insert(insert_point, macro_def[1](name, self.units))
+                self.aperture_macros[name] = macro_def[1](name, self.units)
                 macro_defs[idx] = (name, macro_def[1])
-            for idx in range(insert_point, last_aperture + len(macro_defs) + 1):
-                statement = self.statements[idx]
+            for statement in self.aperture_defs:
                 if isinstance(statement, ADParamStmt):
                     if statement.shape == 'R':
                         statement.shape = macro_defs[RECTANGLE][0]
@@ -114,3 +168,128 @@ class GerberFile(gerber.rs274x.GerberFile):
                                           if x > y else macro_defs[PORTRATE_OBROUND][0] 
                     elif statement.shape == 'P':
                         statement.shape = macro_defs[POLYGON][0]
+
+class GerberContext(FileSettings):
+    TYPE_NONE = 'none'
+    TYPE_AM = 'am'
+    TYPE_AD = 'ad'
+    TYPE_MAIN = 'main'
+    IP_LINEAR = 'lenear'
+    IP_ARC = 'arc'
+    DIR_CLOCKWISE = 'cw'
+    DIR_COUNTERCLOCKWISE = 'ccw'
+
+    ignored_stmt = ('FSParamStmt', 'MOParamStmt', 'ASParamStmt',
+                    'INParamStmt', 'IPParamStmt', 'IRParamStmt',
+                    'MIParamStmt', 'OFParamStmt', 'SFParamStmt',
+                    'LNParamStmt', 'CommentStmt', 'EofStmt',)
+
+    @classmethod
+    def from_settings(cls, settings):
+        return cls(settings.notation, settings.units, settings.zero_suppression,
+                   settings.format, settings.zeros, settings.angle_units)
+
+    def __init__(self, notation='absolute', units='inch',
+                 zero_suppression=None, format=(2, 5), zeros=None,
+                 angle_units='degrees',
+                 name=None,
+                 mirror=(False, False), offset=(0., 0.), scale=(1., 1.),
+                 angle=0., axis='xy'):
+        super(GerberContext, self).__init__(notation, units, zero_suppression, 
+                                      format, zeros, angle_units)
+        self.name = name
+        self.mirror = mirror
+        self.offset = offset
+        self.scale = scale
+        self.angle = angle
+        self.axis = axis
+
+        self.matrix = (1, 0, 
+                       1, 0,
+                       1, 1)
+
+        self.op = None
+        self.interpolation = self.IP_LINEAR
+        self.direction = self.DIR_CLOCKWISE
+        self.x = 0.
+        self.y = 0.
+
+    def normalize_statement(self, stmt):
+        if isinstance(stmt, INParamStmt):
+            self.name = stmt.name
+        elif isinstance(stmt, MIParamStmt):
+            self.mirror = (stmt.a, stmt.b)
+            self._update_matrix()
+        elif isinstance(stmt, OFParamStmt):
+            self.offset = (stmt.a, stmt.b)
+            self._update_matrix()
+        elif isinstance(stmt, SFParamStmt):
+            self.scale = (stmt.a, stmt.b)
+            self._update_matrix()
+        elif isinstance(stmt, ASParamStmt):
+            self.axis = 'yx' if stmt.mode == 'AYBX' else 'xy'
+            self._update_matrix()
+        elif isinstance(stmt, IRParamStmt):
+            self.angle = stmt.angle
+        elif isinstance(stmt, AMParamStmt) and not isinstance(stmt, AMParamStmtEx):
+            stmt = AMParamStmtEx.from_stmt(stmt)
+            return (self.TYPE_AM, [stmt])
+        elif isinstance(stmt, ADParamStmt) and not isinstance(stmt, AMParamStmtEx):
+            stmt = ADParamStmtEx.from_stmt(stmt)
+            return (self.TYPE_AD, [stmt])
+        elif isinstance(stmt, CoordStmt):
+            self._normalize_coordinate(stmt)
+
+        if type(stmt).__name__ in self.ignored_stmt:
+            return (self.TYPE_NONE, None)
+        else:
+            return (self.TYPE_MAIN, [stmt])
+    
+    def _update_matrix(self):
+        if self.axis == 'xy':
+            mx = -1 if self.mirror[0] else 1
+            my = -1 if self.mirror[1] else 1
+            self.matrix = (
+                self.scale[0] * mx, self.offset[0],
+                self.scale[1] * my, self.offset[1],
+                self.scale[0] * mx, self.scale[1] * my)
+        else:
+            mx = -1 if self.mirror[1] else 1
+            my = -1 if self.mirror[0] else 1
+            self.matrix = (
+                self.scale[1] * mx, self.offset[1],
+                self.scale[0] * my, self.offset[0],
+                self.scale[1] * mx, self.scale[0] * my)
+
+    def _normalize_coordinate(self, stmt):
+        if stmt.only_function:
+            if stmt.function == 'G01' or stmt.function == 'G1':
+                self.interpolation = self.IP_LINEAR
+            elif stmt.function == 'G02' or stmt.function == 'G2':
+                self.interpolation = self.IP_ARC
+                self.direction = self.DIR_CLOCKWISE
+                if self.mirror[0] != self.mirror[1]:
+                    self.direction = self.DIR_COUNTERCLOCKWISE
+                    stmt.function = 'G03'
+            elif stmt.function == 'G03' or stmt.function == 'G3':
+                self.interpolation = self.IP_ARC
+                self.direction = self.DIR_COUNTERCLOCKWISE
+                if self.mirror[0] != self.mirror[1]:
+                    self.direction = self.DIR_CLOCKWISE
+                    stmt.function = 'G02'
+            return
+        if self.notation == 'absolute':
+            x = stmt.x if stmt.x is not None else self.x
+            y = stmt.y if stmt.y is not None else self.y
+        else:
+            x = self.x + stmt.x if stmt.x is not None else 0
+            y = self.y + stmt.y if stmt.y is not None else 0
+        self.x, self.y = x, y
+        self.op = stmt.op if stmt.op is not None else self.op
+
+        stmt.op = self.op
+        stmt.x = self.matrix[0] * x + self.matrix[1]
+        stmt.y = self.matrix[2] * y + self.matrix[3]
+        if stmt.op == 'D01' and self.interpolation == self.IP_ARC:
+            stmt.i = self.matrix[4] * stmt.i if stmt.i is not None else 0
+            stmt.j = self.matrix[5] * stmt.j if stmt.j is not None else 0
-- 
cgit 


From 4c4ba0762b30fdd4633a6d2868c508184d681b7d Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Mon, 9 Sep 2019 21:52:52 +0900
Subject: fix issue #2: single quadrant mode is supported

---
 gerberex/rs274x.py | 47 +++++++++++++++++++++++++++++++----------------
 1 file changed, 31 insertions(+), 16 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
index fae0b32..42ae17d 100644
--- a/gerberex/rs274x.py
+++ b/gerberex/rs274x.py
@@ -208,6 +208,7 @@ class GerberContext(FileSettings):
                        1, 0,
                        1, 1)
 
+        self.in_single_quadrant_mode = False
         self.op = None
         self.interpolation = self.IP_LINEAR
         self.direction = self.DIR_CLOCKWISE
@@ -237,6 +238,9 @@ class GerberContext(FileSettings):
         elif isinstance(stmt, ADParamStmt) and not isinstance(stmt, AMParamStmtEx):
             stmt = ADParamStmtEx.from_stmt(stmt)
             return (self.TYPE_AD, [stmt])
+        elif isinstance(stmt, QuadrantModeStmt):
+            self.in_single_quadrant_mode = stmt.mode == 'single-quadrant'
+            stmt.mode = 'multi-quadrant'
         elif isinstance(stmt, CoordStmt):
             self._normalize_coordinate(stmt)
 
@@ -262,22 +266,23 @@ class GerberContext(FileSettings):
                 self.scale[1] * mx, self.scale[0] * my)
 
     def _normalize_coordinate(self, stmt):
+        if stmt.function == 'G01' or stmt.function == 'G1':
+            self.interpolation = self.IP_LINEAR
+        elif stmt.function == 'G02' or stmt.function == 'G2':
+            self.interpolation = self.IP_ARC
+            self.direction = self.DIR_CLOCKWISE
+            if self.mirror[0] != self.mirror[1]:
+                stmt.function = 'G03'
+        elif stmt.function == 'G03' or stmt.function == 'G3':
+            self.interpolation = self.IP_ARC
+            self.direction = self.DIR_COUNTERCLOCKWISE
+            if self.mirror[0] != self.mirror[1]:
+                stmt.function = 'G02'
         if stmt.only_function:
-            if stmt.function == 'G01' or stmt.function == 'G1':
-                self.interpolation = self.IP_LINEAR
-            elif stmt.function == 'G02' or stmt.function == 'G2':
-                self.interpolation = self.IP_ARC
-                self.direction = self.DIR_CLOCKWISE
-                if self.mirror[0] != self.mirror[1]:
-                    self.direction = self.DIR_COUNTERCLOCKWISE
-                    stmt.function = 'G03'
-            elif stmt.function == 'G03' or stmt.function == 'G3':
-                self.interpolation = self.IP_ARC
-                self.direction = self.DIR_COUNTERCLOCKWISE
-                if self.mirror[0] != self.mirror[1]:
-                    self.direction = self.DIR_CLOCKWISE
-                    stmt.function = 'G02'
             return
+
+        last_x = self.x
+        last_y = self.y
         if self.notation == 'absolute':
             x = stmt.x if stmt.x is not None else self.x
             y = stmt.y if stmt.y is not None else self.y
@@ -291,5 +296,15 @@ class GerberContext(FileSettings):
         stmt.x = self.matrix[0] * x + self.matrix[1]
         stmt.y = self.matrix[2] * y + self.matrix[3]
         if stmt.op == 'D01' and self.interpolation == self.IP_ARC:
-            stmt.i = self.matrix[4] * stmt.i if stmt.i is not None else 0
-            stmt.j = self.matrix[5] * stmt.j if stmt.j is not None else 0
+            qx, qy = 1, 1
+            if self.in_single_quadrant_mode:
+                if self.direction == self.DIR_CLOCKWISE:
+                    qx = 1 if y > last_y else -1
+                    qy = 1 if x < last_x else -1
+                else:
+                    qx = 1 if y < last_y else -1
+                    qy = 1 if x > last_x else -1
+                if last_x == x and last_y == y:
+                    qx, qy = 0, 0
+            stmt.i = qx * self.matrix[4] * stmt.i if stmt.i is not None else 0
+            stmt.j = qy * self.matrix[5] * stmt.j if stmt.j is not None else 0
-- 
cgit 


From 00351ebe277aeb90e7463d1b0bd55402249c4687 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Thu, 12 Sep 2019 23:44:50 +0900
Subject: add IP command handling function

---
 gerberex/rs274x.py | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

(limited to 'gerberex')

diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
index 42ae17d..3a3a712 100644
--- a/gerberex/rs274x.py
+++ b/gerberex/rs274x.py
@@ -53,6 +53,8 @@ class GerberFile(gerber.rs274x.GerberFile):
                 self.main_statements.extend(stmts)
         if self.context.angle != 0:
             self.rotate(self.context.angle)
+        if self.context.is_negative:
+            self.nagate_polarity()
         self.context.notation = 'absolute'
         self.context.zeros = 'trailing'
 
@@ -128,6 +130,11 @@ class GerberFile(gerber.rs274x.GerberFile):
                 statement.x = last_rx
                 statement.y = last_ry
     
+    def nagate_polarity(self):
+        for statement in self.main_statements:
+            if isinstance(statement, LPParamStmt):
+                statement.lp = 'dark' if statement.lp == 'clear' else 'clear'
+    
     def _generalize_aperture(self):
         RECTANGLE = 0
         LANDSCAPE_OBROUND = 1
@@ -208,6 +215,9 @@ class GerberContext(FileSettings):
                        1, 0,
                        1, 1)
 
+        self.is_negative = False
+        self.is_first_coordinate = True
+        self.no_polarity = True
         self.in_single_quadrant_mode = False
         self.op = None
         self.interpolation = self.IP_LINEAR
@@ -216,6 +226,7 @@ class GerberContext(FileSettings):
         self.y = 0.
 
     def normalize_statement(self, stmt):
+        additional_stmts = None
         if isinstance(stmt, INParamStmt):
             self.name = stmt.name
         elif isinstance(stmt, MIParamStmt):
@@ -241,14 +252,24 @@ class GerberContext(FileSettings):
         elif isinstance(stmt, QuadrantModeStmt):
             self.in_single_quadrant_mode = stmt.mode == 'single-quadrant'
             stmt.mode = 'multi-quadrant'
+        elif isinstance(stmt, IPParamStmt):
+            self.is_negative = stmt.ip == 'negative'
+        elif isinstance(stmt, LPParamStmt):
+            self.no_polarity = False
         elif isinstance(stmt, CoordStmt):
             self._normalize_coordinate(stmt)
+            if self.is_first_coordinate:
+                self.is_first_coordinate = False
+                if self.no_polarity:
+                    additional_stmts = [LPParamStmt('LP', 'dark'), stmt]
 
         if type(stmt).__name__ in self.ignored_stmt:
             return (self.TYPE_NONE, None)
+        elif additional_stmts is not None:
+            return (self.TYPE_MAIN, additional_stmts)
         else:
             return (self.TYPE_MAIN, [stmt])
-    
+
     def _update_matrix(self):
         if self.axis == 'xy':
             mx = -1 if self.mirror[0] else 1
-- 
cgit 


From fc3f1a23b87d9c4e51967abb0ed4107daa2be5cf Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sat, 28 Sep 2019 17:40:09 +0900
Subject: improve DXF file handling functions: - DM_LINE mode support to
 generate Excellon routing sequence - DM_MOUSE_BITES mode support to generate
 mouse bites along all path also,  not only line object

---
 gerberex/dxf.py      | 327 ++++++++++++++++++++-------------------------------
 gerberex/dxf_path.py | 174 +++++++++++++++++++--------
 2 files changed, 253 insertions(+), 248 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index ae543ae..8f0b984 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -12,7 +12,7 @@ from gerber.gerber_statements import ADParamStmt
 from gerber.excellon_statements import ExcellonTool
 from gerber.excellon_statements import CoordinateStmt
 from gerberex.utility import is_equal_point, is_equal_value
-from gerberex.dxf_path import generate_closed_paths
+from gerberex.dxf_path import generate_paths
 from gerberex.excellon import write_excellon_header
 from gerberex.rs274x import write_gerber_header
 
@@ -25,12 +25,6 @@ class DxfStatement(object):
         self.end = None
         self.is_closed = False
 
-    def to_gerber(self, settings=None, pitch=0, width=0):
-        pass
-
-    def to_excellon(self, settings=None, pitch=0, width=0):
-        pass
-
     def to_inch(self):
         pass
     
@@ -61,38 +55,6 @@ class DxfLineStatement(DxfStatement):
         super(DxfLineStatement, self).__init__(entity)
         self.start = start
         self.end = end
-    
-    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
-        if pitch == 0:
-            x0, y0 = self.start
-            x1, y1 = self.end
-            return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format(
-                write_gerber_value(x0, settings.format,
-                                   settings.zero_suppression),
-                write_gerber_value(y0, settings.format,
-                                   settings.zero_suppression),
-                write_gerber_value(x1, settings.format,
-                                   settings.zero_suppression),
-                write_gerber_value(y1, settings.format,
-                                   settings.zero_suppression)
-            )
-        else:
-            gstr = ""
-            for p in self._dots(pitch, width):
-                gstr += 'X{0}Y{1}D03*\n'.format(
-                    write_gerber_value(p[0], settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(p[1], settings.format, 
-                                       settings.zero_suppression))
-            return gstr
-
-    def to_excellon(self, settings=FileSettings(), pitch=0, width=0):
-        if not pitch:
-            return
-        gstr = ""
-        for p in self._dots(pitch, width):
-            gstr += CoordinateStmt(x=p[0], y=p[1]).to_excellon(settings) + '\n'
-        return gstr
 
     def to_inch(self):
         self.start = (
@@ -119,7 +81,7 @@ class DxfLineStatement(DxfStatement):
         self.start = self.end
         self.end = pt
 
-    def _dots(self, pitch, width):
+    def dots(self, pitch, width, offset=0):
         x0, y0 = self.start
         x1, y1 = self.end
         y1 = self.end[1]
@@ -128,13 +90,18 @@ class DxfLineStatement(DxfStatement):
         l = sqrt(xp * xp + yp * yp)
         xd = xp * pitch / l
         yd = yp * pitch / l
+        x0 += xp * offset / l
+        y0 += yp * offset / l
 
-        d = 0;
-        while d < l + width / 2:
-            yield (x0, y0)
-            x0 += xd
-            y0 += yd
-            d += pitch
+        if offset > l +  width / 2:
+            return (None, offset - l)
+        else:
+            d = offset;
+            while d < l + width / 2:
+                yield ((x0, y0), d - l)
+                x0 += xd
+                y0 += yd
+                d += pitch
 
     def offset(self, offset_x, offset_y):
         self.start = (self.start[0] + offset_x, self.start[1] + offset_y)
@@ -144,104 +111,34 @@ class DxfLineStatement(DxfStatement):
         self.start = rotate_point(self.start, angle, center)
         self.end = rotate_point(self.end, angle, center)
 
-class DxfCircleStatement(DxfStatement):
-    def __init__(self, entity):
-        super(DxfCircleStatement, self).__init__(entity)
-        self.radius = self.entity.radius
-        self.center = (self.entity.center[0], self.entity.center[1])
-        self.start = (self.center[0] + self.radius, self.center[1])
-        self.end = self.start
-        self.is_closed = True
-
-    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
-        if pitch:
-            return
-        r = self.radius
-        x0, y0 = self.center
-        return 'G01*\nX{0}Y{1}D02*\n' \
-               'G75*\nG03*\nX{2}Y{3}I{4}J{5}D01*'.format(
-            write_gerber_value(x0 + r, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(y0, settings.format,
-                               settings.zero_suppression),
-
-            write_gerber_value(x0 + r, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(y0, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(-r, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(0, settings.format,
-                               settings.zero_suppression)
-        )
-
-    def to_inch(self):
-        self.radius = inch(self.radius)
-        self.center = (
-            inch(self.center[0]), inch(self.center[1]))
-
-    def to_metric(self):
-        self.radius = metric(self.radius)
-        self.center = (
-            metric(self.center[0]), metric(self.center[1]))
-
-    def is_equal_to(self, target, error_range=0):
-        if not isinstance(target, DxfCircleStatement):
-            return False
-        return is_equal_point(self.center, target.enter, error_range) and \
-               is_equal_value(self.radius, target.radius)
-
-    def reverse(self):
-        pass
-
-    def offset(self, offset_x, offset_y):
-        self.center = (self.center[0] + offset_x, self.center[1] + offset_y)
-
-    def rotate(self, angle, center=(0, 0)):
-        self.center = rotate_point(self.center, angle, center)
-
 class DxfArcStatement(DxfStatement):
     def __init__(self, entity):
         super(DxfArcStatement, self).__init__(entity)
-        self.start_angle = self.entity.start_angle
-        self.end_angle = self.entity.end_angle
-        self.radius = self.entity.radius
-        self.center = (self.entity.center[0], self.entity.center[1])
-        self.start = (
-            self.center[0] + self.radius * cos(self.start_angle / 180. * pi),
-            self.center[1] + self.radius * sin(self.start_angle / 180. * pi),
-        )
-        self.end = (
-            self.center[0] + self.radius * cos(self.end_angle / 180. * pi),
-            self.center[1] + self.radius * sin(self.end_angle / 180. * pi),
-        )
-        angle = self.end_angle - self.start_angle
-        self.is_closed = angle >= 360 or angle <= -360
-
-    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
-        if pitch:
-            return
-        x0 = self.center[0]
-        y0 = self.center[1]
-        start_x, start_y = self.start
-        end_x, end_y = self.end
-
-        return 'G01*\nX{0}Y{1}D02*\n' \
-               'G75*\nG{2}*\nX{3}Y{4}I{5}J{6}D01*'.format(
-            write_gerber_value(start_x, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(start_y, settings.format,
-                               settings.zero_suppression),
-            '02' if self.start_angle > self.end_angle else '03',
-            write_gerber_value(end_x, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(end_y, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(x0 - start_x, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(y0 - start_y, settings.format,
-                               settings.zero_suppression)
-        )
+        if entity.dxftype == 'CIRCLE':
+            self.radius = self.entity.radius
+            self.center = (self.entity.center[0], self.entity.center[1])
+            self.start = (self.center[0] + self.radius, self.center[1])
+            self.end = self.start
+            self.start_angle = 0
+            self.end_angle = -360
+            self.is_closed = True
+        elif entity.dxftype == 'ARC':
+            self.start_angle = self.entity.start_angle
+            self.end_angle = self.entity.end_angle
+            self.radius = self.entity.radius
+            self.center = (self.entity.center[0], self.entity.center[1])
+            self.start = (
+                self.center[0] + self.radius * cos(self.start_angle / 180. * pi),
+                self.center[1] + self.radius * sin(self.start_angle / 180. * pi),
+            )
+            self.end = (
+                self.center[0] + self.radius * cos(self.end_angle / 180. * pi),
+                self.center[1] + self.radius * sin(self.end_angle / 180. * pi),
+            )
+            angle = self.end_angle - self.start_angle
+            self.is_closed = angle >= 360 or angle <= -360
+        else:
+            raise Exception('invalid DXF type was specified')
 
     def to_inch(self):
         self.radius = inch(self.radius)
@@ -274,6 +171,28 @@ class DxfArcStatement(DxfStatement):
         self.start = self.end
         self.end = tmp
 
+    def dots(self, pitch, width, offset=0):
+        angle = self.end_angle - self.start_angle
+        afactor = 1 if angle > 0 else -1
+        aangle = angle * afactor
+        L = 2 * pi * self.radius
+        l = L * aangle / 360
+        pangle = pitch / L * 360
+        wangle = width / L * 360
+        oangle = offset / L * 360
+
+        if offset > l + width / 2:
+            yield (None, offset - l)
+        else:
+            da = oangle
+            while da < aangle + wangle / 2:
+                cangle = self.start_angle + da * afactor
+                x = self.radius * cos(cangle / 180 * pi) + self.center[0]
+                y = self.radius * sin(cangle / 180 * pi) + self.center[1]
+                remain = (da - aangle) / 360 * L
+                yield((x, y), remain)
+                da += pangle
+
     def offset(self, offset_x, offset_y):
         self.center = (self.center[0] + offset_x, self.center[1] + offset_y)
         self.start = (self.start[0] + offset_x, self.start[1] + offset_y)
@@ -296,36 +215,30 @@ class DxfPolylineStatement(DxfStatement):
         else:
             self.end = (self.entity.points[-1][0], self.entity.points[-1][1])
 
-    def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
-        if pitch:
-            return
-        x0 = self.entity.points[0][0]
-        y0 = self.entity.points[0][1]
-        b = self.entity.bulge[0]
-        gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
-            write_gerber_value(x0, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(y0, settings.format,
-                               settings.zero_suppression),
-        )
-        
+    def disassemble(self):
+        class Item:
+            pass
+
         def ptseq():
             for i in range(1, len(self.entity.points)):
                 yield i
             if self.entity.is_closed:
                 yield 0
-        
+
+        x0 = self.entity.points[0][0]
+        y0 = self.entity.points[0][1]
+        b = self.entity.bulge[0]
         for idx in ptseq():
             pt = self.entity.points[idx]
             x1 = pt[0]
             y1 = pt[1]
             if b == 0:
-                gerber += '\nG01*\nX{0}Y{1}D01*'.format(
-                    write_gerber_value(x1, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(y1, settings.format,
-                                       settings.zero_suppression),
-                )
+                item = Item()
+                item.dxftype = 'LINE'
+                item.start = (x0, y0)
+                item.end = (x1, y1)
+                item.is_closed = False
+                yield DxfLineStatement.from_entity(item)
             else:
                 ang = 4 * atan(b)
                 xm = x0 + x1
@@ -334,24 +247,27 @@ class DxfPolylineStatement(DxfStatement):
                 xc = (xm - t * (y1 - y0)) / 2
                 yc = (ym + t * (x1 - x0)) / 2
                 r = sqrt((x0 - xc)*(x0 - xc) + (y0 - yc)*(y0 - yc))
-
-                gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
-                    '03' if ang > 0 else '02',
-                    write_gerber_value(x1, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(y1, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(xc - x0, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(yc - y0, settings.format,
-                                       settings.zero_suppression)
-                )
+                rx0 = x0 - xc
+                ry0 = y0 - yc
+                rc = max(min(rx0 / r, 1.0), -1.0)
+                start_angle = acos(rc) if ry0 > 0 else 2 * pi - acos(rc)
+                start_angle *= 180 / pi
+                end_angle = start_angle + ang * 180 / pi
+
+                item = Item()
+                item.dxftype = 'ARC'
+                item.start = (x0, y0)
+                item.end = (x1, y1)
+                item.start_angle = start_angle
+                item.end_angle = end_angle
+                item.radius = r
+                item.center = (xc, yc)
+                item.is_closed = end_angle - start_angle >= 360
+                yield DxfArcStatement(item)
 
             x0 = x1
             y0 = y1
             b = self.entity.bulge[idx]
-        
-        return gerber
 
     def to_inch(self):
         self.start = (inch(self.start[0]), inch(self.start[1]))
@@ -376,7 +292,6 @@ class DxfPolylineStatement(DxfStatement):
         for idx in range(len(self.entity.points)):
             self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center)
 
-
 class DxfStatements(object):
     def __init__(self, statements, units, dcode=10, draw_mode=None):
         if draw_mode == None:
@@ -388,7 +303,7 @@ class DxfStatements(object):
         self.width = 0
         self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR
         self.statements = statements
-        self.paths = generate_closed_paths(self.statements, self.error_range)
+        self.close_paths, self.open_paths = generate_paths(self.statements, self.error_range)
 
     @property
     def units(self):
@@ -401,58 +316,62 @@ class DxfStatements(object):
             yield 'D{0}*'.format(self.dcode)
             if self.draw_mode == DxfFile.DM_FILL:
                 yield 'G36*'
-                for statement in self.statements:
-                    if isinstance(statement, DxfCircleStatement) or \
-                        (isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed):
-                        yield statement.to_gerber(settings)
-                for path in self.paths:
+                for path in self.close_paths:
                     yield path.to_gerber(settings)
                 yield 'G37*'
             else:
-                for statement in self.statements:
-                    yield statement.to_gerber(
-                        settings, 
-                        pitch=self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0,
-                        width=self.width)
+                pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0
+                for path in self.open_paths:
+                    yield path.to_gerber(settings, pitch=pitch, width=self.width)
+                for path in self.close_paths:
+                    yield path.to_gerber(settings, pitch=pitch, width=self.width)
 
         return '\n'.join(gerbers())
 
     def to_excellon(self, settings=FileSettings()):
-        if not self.draw_mode == DxfFile.DM_MOUSE_BITES:
+        if self.draw_mode == DxfFile.DM_FILL:
             return
         def drills():
-            for statement in self.statements:
-                if isinstance(statement, DxfLineStatement):
-                    yield statement.to_excellon(settings, pitch=self.pitch, width=self.width)
+            pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0
+            for path in self.open_paths:
+                yield path.to_excellon(settings, pitch=pitch, width=self.width)
+            for path in self.close_paths:
+                yield path.to_excellon(settings, pitch=pitch, width=self.width)
         return '\n'.join(drills())
 
     def to_inch(self):
         if self._units == 'metric':
             self._units = 'inch'
             self.pitch = inch(self.pitch)
+            self.width = inch(self.width)
             self.error_range = inch(self.error_range)
-            for statement in self.statements:
-                statement.to_inch()
-            for path in self.paths:
+            for path in self.open_paths:
+                path.to_inch()
+            for path in self.close_paths:
                 path.to_inch()
 
     def to_metric(self):
         if self._units == 'inch':
             self._units = 'metric'
             self.pitch = metric(self.pitch)
+            self.width = metric(self.width)
             self.error_range = metric(self.error_range)
-            for statement in self.statements:
-                statement.to_metric()
-            for path in self.paths:
+            for path in self.open_paths:
+                path.to_metric()
+            for path in self.close_paths:
                 path.to_metric()
     
     def offset(self, offset_x, offset_y):
-        for statement in self.statements:
-            statement.offset(offset_x, offset_y)
+        for path in self.open_paths:
+            path.offset(offset_x, offset_y)
+        for path in self.close_paths:
+            path.offset(offset_x, offset_y)
 
     def rotate(self, angle, center=(0, 0)):
-        for statement in self.statements:
-            statement.rotate(angle, center)
+        for path in self.open_paths:
+            path.rotate(angle, center)
+        for path in self.close_paths:
+            path.rotate(angle, center)
 
 class DxfFile(CamFile):
     DM_LINE = 0
@@ -483,7 +402,7 @@ class DxfFile(CamFile):
             elif entity.dxftype == 'LINE':
                 statements.append(DxfLineStatement.from_entity(entity))
             elif entity.dxftype == 'CIRCLE':
-                statements.append(DxfCircleStatement(entity))
+                statements.append(DxfArcStatement(entity))
             elif entity.dxftype == 'ARC':
                 statements.append(DxfArcStatement(entity))
         
@@ -513,6 +432,10 @@ class DxfFile(CamFile):
         self._draw_mode = draw_mode
         
         self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
+        if settings.units == 'inch':
+            self.aperture.to_inch()
+        else:
+            self.aperture.to_metric()
         self.statements = DxfStatements(
             statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
 
diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
index ca48d00..0a92287 100644
--- a/gerberex/dxf_path.py
+++ b/gerberex/dxf_path.py
@@ -6,10 +6,11 @@
 from gerber.utils import inch, metric, write_gerber_value
 from gerber.cam import FileSettings
 from gerberex.utility import is_equal_point, is_equal_value
+from gerberex.excellon import CoordinateStmtEx
 
 class DxfPath(object):
-    def __init__(self, statement, error_range=0):
-        self.statements = [statement]
+    def __init__(self, statements, error_range=0):
+        self.statements = statements
         self.error_range = error_range
     
     @property
@@ -22,8 +23,10 @@ class DxfPath(object):
 
     @property
     def is_closed(self):
-        return len(self.statements) > 1 and \
-               is_equal_point(self.start, self.end, self.error_range)
+        if len(self.statements) == 1:
+            return self.statements[0].is_closed
+        else:
+            return is_equal_point(self.start, self.end, self.error_range)
     
     def is_equal_to(self, target, error_range=0):
         if not isinstance(target, DxfPath):
@@ -43,12 +46,31 @@ class DxfPath(object):
                    return False
             return True
         return False
+    
+    def contain(self, target, error_range=0):
+        for statement in self.statements:
+            if statement.is_equal_to(target, error_range):
+                return True
+        else:
+            return False
 
     def to_inch(self):
         self.error_range = inch(self.error_range)
+        for statement in self.statements:
+            statement.to_inch()
 
     def to_metric(self):
         self.error_range = metric(self.error_range)
+        for statement in self.statements:
+            statement.to_metric()
+
+    def offset(self, offset_x, offset_y):
+        for statement in self.statements:
+            statement.offset(offset_x, offset_y)
+
+    def rotate(self, angle, center=(0, 0)):
+        for statement in self.statements:
+            statement.rotate(angle, center)
 
     def reverse(self):
         rlist = []
@@ -133,60 +155,118 @@ class DxfPath(object):
 
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         from gerberex.dxf import DxfArcStatement
-        if pitch:
-            return
+        if pitch == 0:
+            x0, y0 = self.statements[0].start
+            gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
+                write_gerber_value(x0, settings.format,
+                                   settings.zero_suppression),
+                write_gerber_value(y0, settings.format,
+                                   settings.zero_suppression),
+            )
 
-        x0, y0 = self.statements[0].start
-        gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
-            write_gerber_value(x0, settings.format,
-                               settings.zero_suppression),
-            write_gerber_value(y0, settings.format,
-                               settings.zero_suppression),
-        )
-
-        for statement in self.statements:
-            x0, y0 = statement.start
-            x1, y1 = statement.end
-            if isinstance(statement, DxfArcStatement):
-                xc, yc = statement.center
-                gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
-                    '03' if statement.end_angle > statement.start_angle else '02',
-                    write_gerber_value(x1, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(y1, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(xc - x0, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(yc - y0, settings.format,
-                                       settings.zero_suppression)
-                )
-            else:
-                gerber += '\nG01*\nX{0}Y{1}D01*'.format(
-                    write_gerber_value(x1, settings.format,
-                                       settings.zero_suppression),
-                    write_gerber_value(y1, settings.format,
+            for statement in self.statements:
+                x0, y0 = statement.start
+                x1, y1 = statement.end
+                if isinstance(statement, DxfArcStatement):
+                    xc, yc = statement.center
+                    gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
+                        '03' if statement.end_angle > statement.start_angle else '02',
+                        write_gerber_value(x1, settings.format,
+                                           settings.zero_suppression),
+                        write_gerber_value(y1, settings.format,
+                                           settings.zero_suppression),
+                        write_gerber_value(xc - x0, settings.format,
+                                           settings.zero_suppression),
+                        write_gerber_value(yc - y0, settings.format,
+                                           settings.zero_suppression)
+                    )
+                else:
+                    gerber += '\nG01*\nX{0}Y{1}D01*'.format(
+                        write_gerber_value(x1, settings.format,
+                                           settings.zero_suppression),
+                        write_gerber_value(y1, settings.format,
+                                           settings.zero_suppression),
+                    )
+        else:
+            def ploter(x, y):
+                return 'X{0}Y{1}D03*\n'.format(
+                    write_gerber_value(x, settings.format,
                                        settings.zero_suppression),
+                    write_gerber_value(y, settings.format,
+                                          settings.zero_suppression),
                 )
+            gerber = self._plot_dots(pitch, width, ploter)
 
         return gerber
 
-def generate_closed_paths(statements, error_range=0):
-    from gerberex.dxf import DxfLineStatement, DxfArcStatement
+    def to_excellon(self, settings=FileSettings(), pitch=0, width=0):
+        from gerberex.dxf import DxfArcStatement
+        if pitch == 0:
+            x, y = self.statements[0].start
+            excellon = 'G00{0}\nM15\n'.format(
+                CoordinateStmtEx(x=x, y=y).to_excellon(settings))
+
+            for statement in self.statements:
+                x, y = statement.end
+                if isinstance(statement, DxfArcStatement):
+                    r = statement.radius
+                    excellon += '{0}{1}\n'.format(
+                        'G03' if statement.end_angle > statement.start_angle else 'G02',
+                        CoordinateStmtEx(x=x, y=y, radius=r).to_excellon(settings))
+                else:
+                    excellon += 'G01{0}\n'.format(
+                        CoordinateStmtEx(x=x, y=y).to_excellon(settings))
+            
+            excellon += 'M16\nG05\n'
+        else:
+            def ploter(x, y):
+                return CoordinateStmtEx(x=x, y=y).to_excellon(settings) + '\n'
+            excellon = self._plot_dots(pitch, width, ploter)
+
+        return excellon
+
+    def _plot_dots(self, pitch, width, ploter):
+        out = ''
+        offset = 0
+        for idx in range(0, len(self.statements)):
+            statement = self.statements[idx]
+            if offset < 0:
+                offset += pitch
+            for dot, offset in statement.dots(pitch, width, offset):
+                if dot is None:
+                    break
+                if offset > 0 and (statement.is_closed or idx != len(self.statements) - 1):
+                    break
+                #if idx == len(self.statements) - 1 and statement.is_closed and offset > -pitch:
+                #    break
+                out += ploter(dot[0], dot[1])
+        return out
+
+
+def generate_paths(statements, error_range=0):
+    from gerberex.dxf import DxfPolylineStatement
+
+    paths = []
+    for statement in filter(lambda s: isinstance(s, DxfPolylineStatement), statements):
+        units = [unit for unit in statement.disassemble()]
+        paths.append(DxfPath(units, error_range))
 
     unique_statements = []
     redundant = 0
-    for statement in statements:
-        for target in unique_statements:
-            if not isinstance(statement, DxfLineStatement) and \
-               not isinstance(statement, DxfArcStatement):
-                break
-            if statement.is_equal_to(target, error_range):
+    for statement in filter(lambda s: not isinstance(s, DxfPolylineStatement), statements):
+        for path in paths:
+            if path.contain(statement):
                 redundant += 1
                 break
         else:
-            unique_statements.append(statement)
+            for target in unique_statements:
+                if statement.is_equal_to(target, error_range):
+                    redundant += 1
+                    break
+            else:
+                unique_statements.append(statement)
 
-    paths = [DxfPath(s, error_range) for s in unique_statements]
+    paths.extend([DxfPath([s], error_range) for s in unique_statements])
 
     prev_paths_num = 0
     while prev_paths_num != len(paths):
@@ -201,5 +281,7 @@ def generate_closed_paths(statements, error_range=0):
                 working.append(mergee)
         prev_paths_num = len(paths)
         paths = working
-    return list(filter(lambda p: p.is_closed, paths))
     
+    closed_path = list(filter(lambda p: p.is_closed, paths))
+    open_path = list(filter(lambda p: not p.is_closed, paths))
+    return (closed_path, open_path)
-- 
cgit 


From 48b35377b1cce897a70d1d037b3e73b616af9be7 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sat, 28 Sep 2019 20:42:11 +0900
Subject: minor refactoring

---
 gerberex/dxf.py      |  2 +-
 gerberex/dxf_path.py | 14 ++++++++------
 2 files changed, 9 insertions(+), 7 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 8f0b984..eca246c 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -120,7 +120,7 @@ class DxfArcStatement(DxfStatement):
             self.start = (self.center[0] + self.radius, self.center[1])
             self.end = self.start
             self.start_angle = 0
-            self.end_angle = -360
+            self.end_angle = 360
             self.is_closed = True
         elif entity.dxftype == 'ARC':
             self.start_angle = self.entity.start_angle
diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
index 0a92287..1307411 100644
--- a/gerberex/dxf_path.py
+++ b/gerberex/dxf_path.py
@@ -202,20 +202,22 @@ class DxfPath(object):
     def to_excellon(self, settings=FileSettings(), pitch=0, width=0):
         from gerberex.dxf import DxfArcStatement
         if pitch == 0:
-            x, y = self.statements[0].start
+            x0, y0 = self.statements[0].start
             excellon = 'G00{0}\nM15\n'.format(
-                CoordinateStmtEx(x=x, y=y).to_excellon(settings))
+                CoordinateStmtEx(x=x0, y=y0).to_excellon(settings))
 
             for statement in self.statements:
-                x, y = statement.end
+                x0, y0 = statement.start
+                x1, y1 = statement.end
                 if isinstance(statement, DxfArcStatement):
-                    r = statement.radius
+                    i = statement.center[0] - x0
+                    j = statement.center[1] - y0
                     excellon += '{0}{1}\n'.format(
                         'G03' if statement.end_angle > statement.start_angle else 'G02',
-                        CoordinateStmtEx(x=x, y=y, radius=r).to_excellon(settings))
+                        CoordinateStmtEx(x=x1, y=y1, i=i, j=j).to_excellon(settings))
                 else:
                     excellon += 'G01{0}\n'.format(
-                        CoordinateStmtEx(x=x, y=y).to_excellon(settings))
+                        CoordinateStmtEx(x=x1, y=y1).to_excellon(settings))
             
             excellon += 'M16\nG05\n'
         else:
-- 
cgit 


From d7a069324222bb8f69adc9b1c815fc9f3f6a29d6 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Mon, 30 Sep 2019 18:52:17 +0900
Subject: fix a issue that coordinate normalization for excellon is imperfect

---
 gerberex/composition.py | 4 +++-
 gerberex/dxf.py         | 5 ++---
 gerberex/excellon.py    | 2 ++
 3 files changed, 7 insertions(+), 4 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/composition.py b/gerberex/composition.py
index 634640e..b5dffb1 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -138,7 +138,9 @@ class DrillComposition(Composition):
                     if num == t.number:
                         yield statement.to_excellon(self.settings)
             yield EndOfProgramStmt().to_excellon()
-        
+
+        self.settings.notation = 'absolute'
+        self.settings.zeros = 'trailing'
         with open(path, 'w') as f:
             gerberex.excellon.write_excellon_header(f, self.settings, self.tools)
             for statement in statements():
diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index eca246c..00b7695 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -475,9 +475,8 @@ class DxfFile(CamFile):
         self.statements.pitch = value
     
     def write(self, filename=None, filetype=FT_RX274X):
-        if self.settings.notation != 'absolute':
-            raise Exception('DXF file\'s notation must be absolute ')
-        
+        self.settings.notation = 'absolute'
+        self.settings.zeros = 'trailing'
         filename = filename if filename is not None else self.filename
         with open(filename, 'w') as f:
             if filetype == self.FT_RX274X:
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 4f867be..570f94e 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -200,6 +200,8 @@ class ExcellonFileEx(ExcellonFile):
             self.units = 'metric'
     
     def write(self, filename=None):
+        self.notation = 'absolute'
+        self.zeros = 'trailing'
         filename = filename if filename is not None else self.filename
         with open(filename, 'w') as f:
             write_excellon_header(f, self.settings, [self.tools[t] for t in self.tools])
-- 
cgit 


From 244fcaa5346f4fad819cc2b72857cfb2c472944a Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Sat, 28 Dec 2019 23:45:33 +0900
Subject: add a function that generate filled gerberdata with representing
 internal shape by fliping polarity

---
 gerberex/dxf.py      | 282 +++++++++++++++++++++++++++++++++++++++++++++++++--
 gerberex/dxf_path.py | 125 ++++++++++++++++++++++-
 gerberex/utility.py  |   9 +-
 3 files changed, 406 insertions(+), 10 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 00b7695..95a3114 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -12,12 +12,88 @@ from gerber.gerber_statements import ADParamStmt
 from gerber.excellon_statements import ExcellonTool
 from gerber.excellon_statements import CoordinateStmt
 from gerberex.utility import is_equal_point, is_equal_value
-from gerberex.dxf_path import generate_paths
+from gerberex.dxf_path import generate_paths, judge_containment
 from gerberex.excellon import write_excellon_header
 from gerberex.rs274x import write_gerber_header
 
 ACCEPTABLE_ERROR = 0.001
 
+def _normalize_angle(start_angle, end_angle):
+    angle = end_angle - start_angle
+    if angle > 0:
+        start = start_angle % 360
+    else:
+        angle = -angle
+        start = end_angle % 360
+    angle = min(angle, 360)
+    start = start - 360 if  start > 180 else start
+
+    regions = []
+    while angle > 0:
+        end = start + angle
+        if  end <= 180:
+            regions.append((start * pi / 180, end * pi / 180))
+            angle = 0
+        else:
+            regions.append((start * pi / 180, pi))
+            angle = end - 180
+            start = -180
+    return regions
+
+def _intersections_of_line_and_circle(start, end, center, radius, error_range):
+    x1 = start[0] - center[0]
+    y1 = start[1] - center[1]
+    x2 = end[0] - center[0]
+    y2 = end[1] - center[1]
+
+    dx = x2 - x1
+    dy = y2 - y1
+    dr = sqrt(dx * dx + dy * dy)
+    D = x1 * y2 - x2 * y1
+
+    D2 = D * D
+    dr2 = dr * dr
+    r2 = radius * radius
+    delta = r2 * dr2 - D2
+    e4 = error_range * error_range * error_range * error_range * 10
+    if delta > - e4 and delta < e4:
+        delta = 0
+    if delta < 0:
+        return None
+
+    sqrt_D = sqrt(delta)
+    E_x = -dx * sqrt_D if dy < 0 else dx * sqrt_D
+    E_y = abs(dy) * sqrt_D
+
+    p1_x = (D * dy + E_x) / dr2
+    p2_x = (D * dy - E_x) / dr2
+    p1_y = (-D * dx + E_y) / dr2
+    p2_y = (-D * dx - E_y) / dr2
+
+    p1_angle = atan2(p1_y, p1_x)
+    p2_angle = atan2(p2_y, p2_x)
+    if dx == 0: 
+        p1_t = (p1_y - y1) / dy
+        p2_t = (p2_y - y1) / dy
+    else:
+        p1_t = (p1_x - x1) / dx
+        p2_t = (p2_x - x1) / dx
+
+    if delta == 0:
+        return (
+            (p1_x + center[0], p1_y + center[1]),
+            None,
+            p1_angle, None,
+            p1_t, None
+        )
+    else:
+        return (
+            (p1_x + center[0], p1_y + center[1]),
+            (p2_x + center[0], p2_y + center[1]),
+            p1_angle, p2_angle,
+            p1_t, p2_t
+        )
+
 class DxfStatement(object):
     def __init__(self, entity):
         self.entity = entity
@@ -51,6 +127,13 @@ class DxfLineStatement(DxfStatement):
         end = (entity.end[0], entity.end[1])
         return cls(entity, start, end)
 
+    @property
+    def bounding_box(self):
+        return (min(self.start[0], self.end[0]),
+                min(self.start[1], self.end[1]),
+                max(self.start[0], self.end[0]),
+                max(self.start[1], self.end[1]))
+
     def __init__(self, entity, start, end):
         super(DxfLineStatement, self).__init__(entity)
         self.start = start
@@ -110,6 +193,53 @@ class DxfLineStatement(DxfStatement):
     def rotate(self, angle, center=(0, 0)):
         self.start = rotate_point(self.start, angle, center)
         self.end = rotate_point(self.end, angle, center)
+    
+    def intersections_with_halfline(self, point_from, point_to, error_range):
+        denominator = (self.end[0] - self.start[0]) * (point_to[1] - point_from[1]) - \
+                      (self.end[1] - self.start[1]) * (point_to[0] - point_from[0])
+        de = error_range * error_range
+        if denominator > -de and denominator < de:
+            return []
+        from_dx = point_from[0] - self.start[0]
+        from_dy = point_from[1] - self.start[1]
+        r = ((point_to[1] - point_from[1]) * from_dx - 
+             (point_to[0] - point_from[0]) * from_dy) / denominator
+        s = ((self.end[1] - self.start[1]) * from_dx -
+             (self.end[0] - self.start[0]) * from_dy) / denominator
+        dx = (self.end[0] - self.start[0])
+        dy = (self.end[1] - self.start[1])
+        le = error_range / sqrt(dx * dx + dy * dy)
+        if s < 0 or r < -le or r > 1 + le:
+            return []
+        
+        pt = (self.start[0] + (self.end[0] - self.start[0]) * r,
+              self.start[1] + (self.end[1] - self.start[1]) * r)
+        if is_equal_point(pt, self.start, error_range):
+            return []
+        else:
+            return [pt]
+
+    def intersections_with_arc(self, center, radius, angle_regions, error_range):
+        intersection = \
+            _intersections_of_line_and_circle(self.start, self.end, center, radius, error_range)
+        if intersection is None:
+            return []
+        else:
+            p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection
+
+        pts = []
+        if p1_t >= 0 and p1_t <= 1:
+            for region in angle_regions:
+                if p1_angle >= region[0] and p1_angle <= region[1]:
+                    pts.append(p1)
+                    break
+        if p2 is not None and p2_t >= 0 and p2_t <= 1:
+            for region in angle_regions:
+                if p2_angle >= region[0] and p2_angle <= region[1]:
+                    pts.append(p2)
+                    break
+
+        return pts
 
 class DxfArcStatement(DxfStatement):
     def __init__(self, entity):
@@ -139,6 +269,12 @@ class DxfArcStatement(DxfStatement):
             self.is_closed = angle >= 360 or angle <= -360
         else:
             raise Exception('invalid DXF type was specified')
+        self.angle_regions = _normalize_angle(self.start_angle, self.end_angle)
+
+    @property
+    def bounding_box(self):
+        return (self.center[0] - self.radius, self.center[1] - self.radius,
+                self.center[0] + self.radius, self.center[1] + self.radius)
 
     def to_inch(self):
         self.radius = inch(self.radius)
@@ -204,6 +340,82 @@ class DxfArcStatement(DxfStatement):
         self.center = rotate_point(self.center, angle, center)
         self.start = rotate_point(self.start, angle, center)
         self.end = rotate_point(self.end, angle, center)
+        self.angle_regions = _normalize_angle(self.start_angle, self.end_angle)
+
+    def intersections_with_halfline(self, point_from, point_to, error_range):
+        intersection = \
+            _intersections_of_line_and_circle(
+                point_from, point_to, self.center, self.radius, error_range)
+        if intersection is None:
+            return []
+        else:
+            p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection
+        
+        if is_equal_point(p1, self.start, error_range):
+            p1 = None
+        elif p2 is not None and is_equal_point(p2, self.start, error_range):
+            p2 = None
+
+        aerror = error_range * self.radius
+        pts = []
+        if p1 is not None and p1_t >= 0 and not is_equal_point(p1, self.start, error_range):
+            for region in self.angle_regions:
+                if p1_angle >= region[0] - aerror and p1_angle <= region[1] + aerror:
+                    pts.append(p1)
+                    break
+        if p2 is not None and p2_t >= 0 and not is_equal_point(p2, self.start, error_range):
+            for region in self.angle_regions:
+                if p2_angle >= region[0] - aerror and p2_angle <= region[1] + aerror:
+                    pts.append(p2)
+                    break
+
+        return pts
+
+    def intersections_with_arc(self, center, radius, angle_regions, error_range):
+        x1 = center[0] - self.center[0]
+        y1 = center[1] - self.center[1]
+        r1 = self.radius
+        r2 = radius
+        cd_sq = x1 * x1 + y1 * y1
+        cd = sqrt(cd_sq)
+        rd = abs(r1 - r2)
+
+        if (cd >= 0 and cd <= rd) or cd >= r1 + r2:
+            return []
+        
+        A = (cd_sq + r1 * r1 - r2 * r2) / 2
+        scale = sqrt(cd_sq * r1 * r1 - A * A) / cd_sq
+        xl = A * x1 / cd_sq
+        xr = y1 * scale
+        yl = A * y1 / cd_sq
+        yr = x1 * scale
+
+        pt1_x = xl + xr
+        pt1_y = yl - yr
+        pt2_x = xl - xr
+        pt2_y = yl + yr
+        pt1_angle1 = atan2(pt1_y, pt1_x)
+        pt1_angle2 = atan2(pt1_y - y1, pt1_x - x1)
+        pt2_angle1 = atan2(pt2_y, pt2_x)
+        pt2_angle2 = atan2(pt2_y - y1, pt2_x - x1)
+
+        aerror = error_range * self.radius
+        pts=[]
+        for region in self.angle_regions:
+            if pt1_angle1 >= region[0] and pt1_angle1 <= region[1]:
+                for region in angle_regions:
+                    if pt1_angle2 >= region[0] - aerror and pt1_angle2 <= region[1] + aerror:
+                        pts.append((pt1_x + self.center[0], pt1_y + self.center[1]))
+                        break
+                break
+        for region in self.angle_regions:
+            if pt2_angle1 >= region[0] and pt2_angle1 <= region[1]:
+                for region in angle_regions:
+                    if pt2_angle2 >= region[0] - aerror and pt2_angle2 <= region[1] + aerror:
+                        pts.append((pt2_x + self.center[0], pt2_y + self.center[1]))
+                        break
+                break
+        return pts
 
 class DxfPolylineStatement(DxfStatement):
     def __init__(self, entity):
@@ -293,31 +505,69 @@ class DxfPolylineStatement(DxfStatement):
             self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center)
 
 class DxfStatements(object):
-    def __init__(self, statements, units, dcode=10, draw_mode=None):
-        if draw_mode == None:
+    def __init__(self, statements, units, dcode=10, draw_mode=None, fill_mode=None):
+        if draw_mode is None:
             draw_mode = DxfFile.DM_LINE
+        if fill_mode is None:
+            fill_mode = DxfFile.FM_TURN_OVER
         self._units = units
         self.dcode = dcode
         self.draw_mode = draw_mode
+        self.fill_mode = fill_mode
         self.pitch = inch(1) if self._units == 'inch' else 1
         self.width = 0
         self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR
-        self.statements = statements
+        self.statements = list(filter(
+            lambda i: not (isinstance(i, DxfLineStatement) and \
+                          is_equal_point(i.start, i.end, self.error_range)),
+            statements
+        ))
         self.close_paths, self.open_paths = generate_paths(self.statements, self.error_range)
+        self.sorted_close_paths = []
+        self.polarity = True # True means dark, False means clear
 
     @property
     def units(self):
         return _units
 
+    def _polarity_command(self, polarity=None):
+        if polarity is None:
+            polarity = self.polarity
+        return '%LPD*%' if polarity else '%LPC*%'
+
+    def _prepare_sorted_close_paths(self):
+        if self.sorted_close_paths:
+            return
+        for i in range(0, len(self.close_paths)):
+            for j in range(i + 1, len(self.close_paths)):
+                containee, container = judge_containment(
+                    self.close_paths[i], self.close_paths[j], self.error_range)
+                if containee is not None:
+                    containee.containers.append(container)
+        self.sorted_close_paths = sorted(self.close_paths, key=lambda path: len(path.containers))
+
     def to_gerber(self, settings=FileSettings()):
         def gerbers():
             yield 'G75*'
-            yield '%LPD*%'
+            yield self._polarity_command()
             yield 'D{0}*'.format(self.dcode)
             if self.draw_mode == DxfFile.DM_FILL:
                 yield 'G36*'
-                for path in self.close_paths:
-                    yield path.to_gerber(settings)
+                if self.fill_mode == DxfFile.FM_TURN_OVER:
+                    self._prepare_sorted_close_paths()
+                    polarity = self.polarity
+                    level = 0
+                    for path in self.sorted_close_paths:
+                        if len(path.containers) > level:
+                            level = len(path.containers)
+                            polarity = not polarity
+                            yield 'G37*'
+                            yield self._polarity_command(polarity)
+                            yield 'G36*'
+                        yield path.to_gerber(settings)
+                else:
+                    for path in self.close_paths:
+                        yield path.to_gerber(settings)
                 yield 'G37*'
             else:
                 pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0
@@ -378,6 +628,9 @@ class DxfFile(CamFile):
     DM_FILL = 1
     DM_MOUSE_BITES = 2
 
+    FM_SIMPLE = 0
+    FM_TURN_OVER = 1
+
     FT_RX274X = 0
     FT_EXCELLON = 1
 
@@ -430,6 +683,7 @@ class DxfFile(CamFile):
 
         super(DxfFile, self).__init__(settings=settings, filename=filename)
         self._draw_mode = draw_mode
+        self._fill_mode = self.FM_TURN_OVER
         
         self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
         if settings.units == 'inch':
@@ -437,7 +691,7 @@ class DxfFile(CamFile):
         else:
             self.aperture.to_metric()
         self.statements = DxfStatements(
-            statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
+            statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode, fill_mode=self.filename)
 
     @property
     def dcode(self):
@@ -466,6 +720,15 @@ class DxfFile(CamFile):
         self._draw_mode = value
         self.statements.draw_mode = value
 
+    @property
+    def fill_mode(self):
+        return self._fill_mode
+    
+    @fill_mode.setter
+    def fill_mode(self, value):
+        self._fill_mode = value
+        self.statements.fill_mode = value
+
     @property
     def pitch(self):
         return self.statements.pitch
@@ -512,6 +775,9 @@ class DxfFile(CamFile):
     def rotate(self, angle, center=(0, 0)):
         self.statements.rotate(angle, center)
 
+    def negate_polarity(self):
+        self.statements.polarity = not self.statements.polarity
+
 def loads(data, filename=None):
     if sys.version_info.major == 2:
         data = unicode(data)
diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
index 1307411..bb620ff 100644
--- a/gerberex/dxf_path.py
+++ b/gerberex/dxf_path.py
@@ -5,13 +5,17 @@
 
 from gerber.utils import inch, metric, write_gerber_value
 from gerber.cam import FileSettings
-from gerberex.utility import is_equal_point, is_equal_value
+from gerberex.utility import is_equal_point, is_equal_value, normalize_vec2d, dot_vec2d
 from gerberex.excellon import CoordinateStmtEx
 
 class DxfPath(object):
     def __init__(self, statements, error_range=0):
         self.statements = statements
         self.error_range = error_range
+        self.bounding_box = statements[0].bounding_box
+        self.containers = []
+        for statement in statements[1:]:
+            self._merge_bounding_box(statement.bounding_box)
     
     @property
     def start(self):
@@ -116,12 +120,15 @@ class DxfPath(object):
             if j > 0:
                 del mergee[-j]
                 del self.statements[0:j]
+            for statement in mergee:
+                self._merge_bounding_box(statement.bounding_box)
             self.statements.extend(mergee)
             return True
         else:
             if self.statements[-1].is_equal_to(element, error_range) or \
                self.statements[0].is_equal_to(element, error_range):
                 return False
+            self._merge_bounding_box(element.bounding_box)
             self.statements.appen(element)
             return True
 
@@ -153,6 +160,21 @@ class DxfPath(object):
             self.statements.insert(0, element)
             return True
 
+    def _merge_bounding_box(self, box):
+        self.bounding_box = (min(self.bounding_box[0], box[0]),
+                             min(self.bounding_box[1], box[1]),
+                             max(self.bounding_box[2], box[2]),
+                             max(self.bounding_box[3], box[3]))
+
+    def may_be_in_collision(self, path):
+        if self.bounding_box[0] >= path.bounding_box[2] or \
+           self.bounding_box[1] >= path.bounding_box[3] or \
+           self.bounding_box[2] <= path.bounding_box[0] or \
+           self.bounding_box[3] <= path.bounding_box[1]:
+            return False
+        else:
+            return True
+
     def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
         from gerberex.dxf import DxfArcStatement
         if pitch == 0:
@@ -244,7 +266,61 @@ class DxfPath(object):
                 out += ploter(dot[0], dot[1])
         return out
 
+    def intersections_with_halfline(self, point_from, point_to, error_range=0):
+        def calculator(statement):
+            return statement.intersections_with_halfline(point_from, point_to, error_range)
+        def validator(pt, statement, idx):
+            if is_equal_point(pt, statement.end, error_range) and \
+                not self._judge_cross(point_from, point_to, idx, error_range):
+                    return False
+            return True
+        return self._collect_intersections(calculator, validator, error_range)
+
+    def intersections_with_arc(self, center, radius, angle_regions, error_range=0):
+        def calculator(statement):
+            return statement.intersections_with_arc(center, radius, angle_regions, error_range)
+        return self._collect_intersections(calculator, None, error_range)
 
+    def _collect_intersections(self, calculator, validator, error_range):
+        allpts = []
+        last = allpts
+        for i in range(0, len(self.statements)):
+            statement = self.statements[i]
+            cur = calculator(statement)
+            if cur:
+                for pt in cur:
+                    for dest in allpts:
+                        if is_equal_point(pt, dest, error_range):
+                            break
+                    else:
+                        if validator is not None and not validator(pt, statement, i):
+                            continue
+                        allpts.append(pt)
+            last = cur
+        return allpts
+    
+    def _judge_cross(self, from_pt, to_pt, index, error_range):
+        standard = normalize_vec2d((to_pt[0] - from_pt[0], to_pt[1] - from_pt[1]))
+        normal = (standard[1], standard[0])
+        def statements():
+            for i in range(index, len(self.statements)):
+                yield self.statements[i]
+            for i in range(0, index):
+                yield self.statements[i]
+        dot_standard = None
+        for statement in statements():
+            tstart = statement.start
+            tend = statement.end
+            target = normalize_vec2d((tend[0] - tstart[0], tend[1] - tstart[1]))
+            dot= dot_vec2d(normal, target)
+            if dot_standard is None:
+                dot_standard = dot
+                continue
+            if is_equal_point(standard, target, error_range):
+                continue
+            return (dot_standard > 0 and dot > 0) or (dot_standard < 0 and dot < 0)
+        raise Exception('inconsistensy is detected while cross judgement between paths')
+            
 def generate_paths(statements, error_range=0):
     from gerberex.dxf import DxfPolylineStatement
 
@@ -287,3 +363,50 @@ def generate_paths(statements, error_range=0):
     closed_path = list(filter(lambda p: p.is_closed, paths))
     open_path = list(filter(lambda p: not p.is_closed, paths))
     return (closed_path, open_path)
+
+def judge_containment(path1, path2, error_range=0):
+    from gerberex.dxf import DxfArcStatement, DxfLineStatement
+
+    nocontainment = (None, None)
+    if not path1.may_be_in_collision(path2):
+        return nocontainment
+    
+    def is_in_line_segment(point_from, point_to, point):
+        dx = point_to[0] - point_from[0]
+        ratio = (point[0] - point_from[0]) / dx if dx != 0 else \
+                (point[1] - point_from[1]) / (point_to[1] - point_from[1])
+        return ratio >= 0 and ratio <= 1
+
+    def contain_in_path(statement, path):
+        if isinstance(statement, DxfLineStatement):
+            segment = (statement.start, statement.end)
+        elif isinstance(statement, DxfArcStatement):
+            if statement.start == statement.end:
+                segment = (statement.start, statement.center)
+            else:
+                segment = (statement.start, statement.end)
+        else:
+            raise Exception('invalid dxf statement type')
+        pts = path.intersections_with_halfline(segment[0], segment[1], error_range)
+        if len(pts) % 2 == 0:
+            return False
+        for pt in pts:
+            if is_in_line_segment(segment[0], segment[1], pt):
+                return False
+        if isinstance(statement, DxfArcStatement):
+            pts = path.intersections_with_arc(
+                statement.center, statement.radius, statement.angle_regions, error_range)
+            if len(pts) > 0:
+                return False
+        return True
+    
+    if contain_in_path(path1.statements[0], path2):
+        containment = [path1, path2]
+    elif contain_in_path(path2.statements[0], path1):
+        containment = [path2, path1]
+    else:
+        return nocontainment
+    for i in range(1, len(containment[0].statements)):
+        if not contain_in_path(containment[0].statements[i], containment[1]):
+            return nocontainment
+    return containment
diff --git a/gerberex/utility.py b/gerberex/utility.py
index 4c89fa6..37de5e8 100644
--- a/gerberex/utility.py
+++ b/gerberex/utility.py
@@ -3,7 +3,7 @@
 
 # Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
 
-from math import cos, sin, pi
+from math import cos, sin, pi, sqrt
 
 def rotate(x, y, angle, center):
     x0 = x - center[0]
@@ -18,3 +18,10 @@ def is_equal_value(a, b, error_range=0):
 def is_equal_point(a, b, error_range=0):
     return is_equal_value(a[0], b[0], error_range) and \
         is_equal_value(a[1], b[1], error_range)
+
+def normalize_vec2d(vec):
+    length = sqrt(vec[0] * vec[0] + vec[1] * vec[1])
+    return (vec[0] / length, vec[1] / length)
+
+def dot_vec2d(vec1, vec2):
+    return vec1[0] * vec2[0] + vec1[1] * vec2[1]
\ No newline at end of file
-- 
cgit 


From ca23fbd9534ab3cba3fd7b032816766c1150ebf9 Mon Sep 17 00:00:00 2001
From: Hiroshi Murayama <opiopan@gmail.com>
Date: Mon, 30 Dec 2019 17:51:48 +0900
Subject: fix bugs that fail judgement of path's containment

---
 gerberex/dxf.py      | 20 +++++++++++++++-----
 gerberex/dxf_path.py |  2 +-
 2 files changed, 16 insertions(+), 6 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/dxf.py b/gerberex/dxf.py
index 95a3114..2341092 100644
--- a/gerberex/dxf.py
+++ b/gerberex/dxf.py
@@ -51,12 +51,13 @@ def _intersections_of_line_and_circle(start, end, center, radius, error_range):
     dr = sqrt(dx * dx + dy * dy)
     D = x1 * y2 - x2 * y1
 
+    distance = abs(dy * x1 - dx * y1) / dr
+
     D2 = D * D
     dr2 = dr * dr
     r2 = radius * radius
     delta = r2 * dr2 - D2
-    e4 = error_range * error_range * error_range * error_range * 10
-    if delta > - e4 and delta < e4:
+    if distance > radius - error_range and distance < radius + error_range:
         delta = 0
     if delta < 0:
         return None
@@ -198,7 +199,7 @@ class DxfLineStatement(DxfStatement):
         denominator = (self.end[0] - self.start[0]) * (point_to[1] - point_from[1]) - \
                       (self.end[1] - self.start[1]) * (point_to[0] - point_from[0])
         de = error_range * error_range
-        if denominator > -de and denominator < de:
+        if denominator >= -de and denominator <= de:
             return []
         from_dx = point_from[0] - self.start[0]
         from_dy = point_from[1] - self.start[1]
@@ -356,16 +357,25 @@ class DxfArcStatement(DxfStatement):
         elif p2 is not None and is_equal_point(p2, self.start, error_range):
             p2 = None
 
+        def is_contained(angle, region, error):
+            if angle >= region[0] - error and angle <= region[1] + error:
+                return True
+            if angle < 0 and region[1] > 0:
+                angle = angle + 2 * pi
+            elif angle > 0 and region[0] < 0:
+                angle = angle - 2 * pi
+            return  angle >= region[0] - error and angle <= region[1] + error
+            
         aerror = error_range * self.radius
         pts = []
         if p1 is not None and p1_t >= 0 and not is_equal_point(p1, self.start, error_range):
             for region in self.angle_regions:
-                if p1_angle >= region[0] - aerror and p1_angle <= region[1] + aerror:
+                if is_contained(p1_angle, region, aerror):
                     pts.append(p1)
                     break
         if p2 is not None and p2_t >= 0 and not is_equal_point(p2, self.start, error_range):
             for region in self.angle_regions:
-                if p2_angle >= region[0] - aerror and p2_angle <= region[1] + aerror:
+                if is_contained(p2_angle, region, aerror):
                     pts.append(p2)
                     break
 
diff --git a/gerberex/dxf_path.py b/gerberex/dxf_path.py
index bb620ff..960b054 100644
--- a/gerberex/dxf_path.py
+++ b/gerberex/dxf_path.py
@@ -301,7 +301,7 @@ class DxfPath(object):
     
     def _judge_cross(self, from_pt, to_pt, index, error_range):
         standard = normalize_vec2d((to_pt[0] - from_pt[0], to_pt[1] - from_pt[1]))
-        normal = (standard[1], standard[0])
+        normal = (standard[1], -standard[0])
         def statements():
             for i in range(index, len(self.statements)):
                 yield self.statements[i]
-- 
cgit 


From 3f9295b9d0c2142c46aa238836debdd472811899 Mon Sep 17 00:00:00 2001
From: jaseg <git@jaseg.de>
Date: Sat, 6 Feb 2021 09:38:22 +0100
Subject: Fix support for comments in aperture definitions

---
 gerberex/am_expression.py | 2 ++
 1 file changed, 2 insertions(+)

(limited to 'gerberex')

diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py
index 5400130..f43ba2e 100644
--- a/gerberex/am_expression.py
+++ b/gerberex/am_expression.py
@@ -49,6 +49,8 @@ class AMConstantExpression(AMExpression):
         return self
     
     def to_gerber(self, settings=None):
+        if isinstance(self._value, str):
+            return self._value
         gerber = '%.6g' % self._value
         return '%.6f' % self._value if 'e' in gerber else gerber
 
-- 
cgit 


From 71c371ca680483aa9ef18d2998832460dd43abdf Mon Sep 17 00:00:00 2001
From: jaseg <git@jaseg.de>
Date: Sat, 6 Feb 2021 09:38:49 +0100
Subject: Fix mystery crashes in excellon parser

I don't know this code and I don't get what was intended here, but it
makes way more sense to me this way.
---
 gerberex/excellon.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'gerberex')

diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 570f94e..f7787d3 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -114,8 +114,8 @@ class ExcellonFileEx(ExcellonFile):
                 if isinstance(stmt, ToolSelectionStmt):
                     current_tool = file.tools[stmt.tool]
                 elif isinstance(stmt, DrillModeStmt):
-                    rout = make_rout(status, rout_statements)
-                    rout_statements = []
+                    rout = make_rout(status, rout_nodes)
+                    rout_nodes = []
                     if rout is not None:
                         yield rout
                     status = STAT_DRILL
@@ -137,7 +137,7 @@ class ExcellonFileEx(ExcellonFile):
                     status = STAT_ROUT_DOWN
                 elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt):
                     rout = make_rout(status, rout_nodes)
-                    rout_statements = []
+                    rout_nodes = []
                     if rout is not None:
                         yield rout
                     status = STAT_ROUT_UP
-- 
cgit