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 |