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 |