DXF export of pins + holes
Stefan Schuermans

Stefan Schuermans commited on 2021-02-17 19:13:39
Showing 3 changed files, with 216 additions and 8 deletions.

... ...
@@ -0,0 +1,176 @@
1
+"""
2
+Exporting of GNU PCB files to DXF.
3
+"""
4
+
5
+import ezdxf
6
+import sys
7
+
8
+import pcb_types
9
+
10
+
11
+class DxfFootprintWriter():
12
+    """
13
+    Writer for DXF files from GNU PCB footprints.
14
+    """
15
+
16
+    def __init__(self):
17
+        self._doc = ezdxf.new('R12')
18
+        self._msp = self._doc.modelspace()
19
+        self._layers = {}
20
+        self._addLayer('clearance', 8)  # gray
21
+        self._addLayer('hole_drill', 3)  # green
22
+        self._addLayer('mask', 16)  # dark red
23
+        self._addLayer('name', 7)  # white
24
+        self._addLayer('number', 7)  # white
25
+        self._addLayer('pin_copper', 1)  # red
26
+        self._addLayer('pin_drill', 5)  # blue
27
+
28
+    def _addCircle(self, x: pcb_types.Coordinate, y: pcb_types.Coordinate,
29
+                   d: pcb_types.Coordinate, layer_name: str):
30
+        self._msp.add_circle((x.mm, -y.mm),
31
+                             radius=d.mm / 2,
32
+                             dxfattribs={'layer': layer_name})
33
+
34
+    def _addOctagon(self, cx: pcb_types.Coordinate, cy: pcb_types.Coordinate,
35
+                   size: pcb_types.Coordinate, layer_name: str):
36
+        l = size.mm / 2
37
+        s = l / (1 + 2**.5)
38
+        points = [(cx.mm - s, -cy.mm - l),
39
+                  (cx.mm + s, -cy.mm - l),
40
+                  (cx.mm + l, -cy.mm - s),
41
+                  (cx.mm + l, -cy.mm + s),
42
+                  (cx.mm + s, -cy.mm + l),
43
+                  (cx.mm - s, -cy.mm + l),
44
+                  (cx.mm - l, -cy.mm + s),
45
+                  (cx.mm - l, -cy.mm - s),
46
+                  (cx.mm - s, -cy.mm - l)]
47
+        self._msp.add_polyline2d(points, dxfattribs={'layer': layer_name})
48
+
49
+    def _addSquare(self, cx: pcb_types.Coordinate, cy: pcb_types.Coordinate,
50
+                   size: pcb_types.Coordinate, layer_name: str):
51
+        points = [(cx.mm - size.mm / 2, -cy.mm - size.mm / 2),
52
+                  (cx.mm + size.mm / 2, -cy.mm - size.mm / 2),
53
+                  (cx.mm + size.mm / 2, -cy.mm + size.mm / 2),
54
+                  (cx.mm - size.mm / 2, -cy.mm + size.mm / 2),
55
+                  (cx.mm - size.mm / 2, -cy.mm - size.mm / 2)]
56
+        self._msp.add_polyline2d(points, dxfattribs={'layer': layer_name})
57
+
58
+    def _addLayer(self, name: str, color: int = 7):
59
+        if name in self._layers:
60
+            return
61
+        self._layers[name] = self._doc.layers.new(name=name,
62
+                                                  dxfattribs={
63
+                                                      'linetype': 'Continuous',
64
+                                                      'color': color
65
+                                                  })
66
+
67
+    def _addText(self,
68
+                 x: pcb_types.Coordinate,
69
+                 y: pcb_types.Coordinate,
70
+                 t: str,
71
+                 layer_name: str,
72
+                 height: pcb_types.Coordinate = None,
73
+                 align: str = None):
74
+        dxfattribs = {'layer': layer_name}
75
+        if height is not None:
76
+            dxfattribs['height'] = height.mm
77
+        text = self._msp.add_text(t, dxfattribs=dxfattribs)
78
+        kwargs = {}
79
+        if align is not None:
80
+            kwargs['align'] = align
81
+        text.set_pos((x.mm, -y.mm), **kwargs)
82
+
83
+    def _drawHole(self, pin: pcb_types.Pin):
84
+        self._addCircle(pin.r_x, pin.r_y, pin.drill, 'hole_drill')
85
+        self._addCircle(pin.r_x, pin.r_y, pin.thickness + pin.clearance,
86
+                        'clearance')
87
+        self._addCircle(pin.r_x, pin.r_y, pin.mask, 'mask')
88
+        self._drawNameNumber(pin.r_x,
89
+                             pin.r_y,
90
+                             pin.name,
91
+                             pin.number,
92
+                             height=pin.drill / 2)
93
+
94
+    def _drawNameNumber(self,
95
+                        x: pcb_types.Coordinate,
96
+                        y: pcb_types.Coordinate,
97
+                        name: str,
98
+                        number: str,
99
+                        height: pcb_types.Coordinate = None):
100
+        self._addText(x, y, name, 'name', height=height, align='BOTTOM_CENTER')
101
+        self._addText(x,
102
+                      y,
103
+                      number,
104
+                      'number',
105
+                      height=height,
106
+                      align='TOP_CENTER')
107
+
108
+    def _drawPinOctagon(self, pin: pcb_types.Pin):
109
+        self._addOctagon(pin.r_x, pin.r_y, pin.thickness, 'pin_copper')
110
+        self._addCircle(pin.r_x, pin.r_y, pin.drill, 'pin_drill')
111
+        self._addOctagon(pin.r_x, pin.r_y, pin.thickness + pin.clearance,
112
+                        'clearance')
113
+        self._addOctagon(pin.r_x, pin.r_y, pin.mask, 'mask')
114
+        self._drawNameNumber(pin.r_x,
115
+                             pin.r_y,
116
+                             pin.name,
117
+                             pin.number,
118
+                             height=pin.drill / 2)
119
+
120
+    def _drawPinRound(self, pin: pcb_types.Pin):
121
+        self._addCircle(pin.r_x, pin.r_y, pin.thickness, 'pin_copper')
122
+        self._addCircle(pin.r_x, pin.r_y, pin.drill, 'pin_drill')
123
+        self._addCircle(pin.r_x, pin.r_y, pin.thickness + pin.clearance,
124
+                        'clearance')
125
+        self._addCircle(pin.r_x, pin.r_y, pin.mask, 'mask')
126
+        self._drawNameNumber(pin.r_x,
127
+                             pin.r_y,
128
+                             pin.name,
129
+                             pin.number,
130
+                             height=pin.drill / 2)
131
+
132
+    def _drawPinSquare(self, pin: pcb_types.Pin):
133
+        self._addSquare(pin.r_x, pin.r_y, pin.thickness, 'pin_copper')
134
+        self._addCircle(pin.r_x, pin.r_y, pin.drill, 'pin_drill')
135
+        self._addSquare(pin.r_x, pin.r_y, pin.thickness + pin.clearance,
136
+                        'clearance')
137
+        self._addSquare(pin.r_x, pin.r_y, pin.mask, 'mask')
138
+        self._drawNameNumber(pin.r_x,
139
+                             pin.r_y,
140
+                             pin.name,
141
+                             pin.number,
142
+                             height=pin.drill / 2)
143
+
144
+    def drawFootprint(self, fp: pcb_types.Element):
145
+        """
146
+        Draw footprint to DXF.
147
+        """
148
+        # draw entities in header
149
+        # TODO
150
+        # draw entities in body
151
+        type2method = {pcb_types.Pin: self.drawPin}
152
+        for entity in fp.body:
153
+            try:
154
+                method = type2method[type(entity)]
155
+                method(entity)
156
+            except KeyError:
157
+                # TODO
158
+                print(
159
+                    'warning: ignoring unknown entity of'
160
+                    f' type {type(entity).__name__:s}',
161
+                    file=sys.stderr)
162
+
163
+    def drawPin(self, pin: pcb_types.Pin):
164
+        if 'hole' in pin.s_flags:
165
+            return self._drawHole(pin)
166
+        if 'octagon' in pin.s_flags:
167
+            return self._drawPinOctagon(pin)
168
+        if 'square' in pin.s_flags:
169
+            return self._drawPinSquare(pin)
170
+        return self._drawPinRound(pin)
171
+
172
+    def writeDxf(self, file_name: str):
173
+        """
174
+        Write DXF file.
175
+        """
176
+        self._doc.saveas(file_name)
... ...
@@ -1,12 +1,27 @@
1 1
 #! /usr/bin/env python3
2 2
 
3
+import dxf_export
3 4
 import pcb_parser
4 5
 import pcb_types
5 6
 
6
-import ezdxf
7
+import argparse
7 8
 import sys
8 9
 
9 10
 
11
+def parse_args():
12
+    parser = argparse.ArgumentParser('Convert GNU PCB footrpint to DXF.')
13
+    parser.add_argument('-ifp',
14
+                        '--in-footprint',
15
+                        required=True,
16
+                        help='GNU PCB footprint file (input, required)')
17
+    parser.add_argument('-odxf',
18
+                        '--out-dxf',
19
+                        required=True,
20
+                        help='DXF file (output, required)')
21
+    args = parser.parse_args()
22
+    return args
23
+
24
+
10 25
 def read_footprint(file_name: str) -> pcb_types.Element:
11 26
     with open(file_name, 'r') as f:
12 27
         s = f.read()
... ...
@@ -15,16 +30,16 @@ def read_footprint(file_name: str) -> pcb_types.Element:
15 30
     return element
16 31
 
17 32
 
18
-def write_dxf(file_name: str):
19
-    doc = ezdxf.new('R12')
20
-    msp = doc.modelspace()
21
-    msp.add_circle((1, 2), radius=3)
22
-    doc.saveas(file_name)
33
+def write_dxf(fp: pcb_types.Element, file_name: str):
34
+    dfw = dxf_export.DxfFootprintWriter()
35
+    dfw.drawFootprint(fp)
36
+    dfw.writeDxf(file_name)
23 37
 
24 38
 
25 39
 def main():
26
-    fp = read_footprint(sys.argv[1])
27
-    print(fp)
40
+    args = parse_args()
41
+    fp = read_footprint(args.in_footprint)
42
+    write_dxf(fp, args.out_dxf)
28 43
 
29 44
 
30 45
 if __name__ == '__main__':
... ...
@@ -47,6 +47,23 @@ class Coordinate():
47 47
     def __ge__(self, other) -> bool:
48 48
         return self._compare(other) >= 0
49 49
 
50
+    def __add__(self, other):
51
+        assert isinstance(other, Coordinate)
52
+        return Coordinate(self._raw + other._raw)
53
+
54
+    def __mul__(self, other):
55
+        return Coordinate(self._raw * other)
56
+
57
+    def __neg__(self):
58
+        return Coordinate(-self._raw)
59
+
60
+    def __sub__(self, other):
61
+        assert isinstance(other, Coordinate)
62
+        return Coordinate(self._raw - other._raw)
63
+
64
+    def __truediv__(self, other):
65
+        return Coordinate(self._raw / other)
66
+
50 67
     @property
51 68
     def mil(self) -> float:
52 69
         return self._raw / self.MIL
53 70