From 5f1350d4f4e1c190bf0b2db12b8183519b2d3413 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 20 Sep 2023 14:24:15 +0200 Subject: coil gen: add kicad pcb export --- gerbonara/cad/kicad/footprints.py | 7 ++++ gerbonara/cad/kicad/pcb.py | 75 +++++++++++++++++++++++++++++++++++---- twisted_coil_gen_twolayer.py | 43 +++++++++++++--------- 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py index 5c27855..90050ef 100644 --- a/gerbonara/cad/kicad/footprints.py +++ b/gerbonara/cad/kicad/footprints.py @@ -95,6 +95,10 @@ class Line: locked: Flag() = False tstamp: Timestamp = None + def to_graphical_primitive(self, flip=False): + # FIXME flip + return gr.Line(self.start, self.end, self.layer, self.width, self.stroke, self.tstamp) + def render(self, variables=None, cache=None): dasher = Dasher(self) dasher.move(self.start.x, self.start.y) @@ -183,6 +187,9 @@ class Arc: locked: Flag() = False tstamp: Timestamp = None + def to_graphical_primitive(self, flip=False): + # FIXME flip + return gr.Arc(self.start, self.mid, self.end, self.layer, self.width, self.stroke, self.tstamp) def render(self, variables=None, cache=None): mx, my = self.mid.x, self.mid.y diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py index ccda43f..a97986b 100644 --- a/gerbonara/cad/kicad/pcb.py +++ b/gerbonara/cad/kicad/pcb.py @@ -95,8 +95,8 @@ TFBool = YesNoAtom(yes=Atom.true, no=Atom.false) @sexp_type('pcbplotparams') class ExportSettings: - layerselection: Named(Atom) = 0 - plot_on_all_layers_selection: Named(Atom) = 0 + layerselection: Named(Atom) = None + plot_on_all_layers_selection: Named(Atom) = None disableapertmacros: Named(TFBool) = False usegerberextensions: Named(TFBool) = True usegerberattributes: Named(TFBool) = True @@ -246,6 +246,24 @@ class Via: net: Named(int) = 0 tstamp: Timestamp = field(default_factory=Timestamp) + @classmethod + def from_pad(kls, pad): + if pad.type != Atom.thru_hole or pad.shape != Atom.circle: + raise ValueError('Can only convert circular through-hole pads to vias.') + + if pad.drill and (pad.drill.oval or pad.drill.offset): + raise ValueError('Can only convert pads with centered, circular drills to vias.') + + x, y, rot, _flip = pad.abs_pos + return kls(locked=pad.locked, + at=XYCoord(x, y), + size=max(pad.size.x, pad.size.y), + drill=pad.drill.diameter if pad.drill else 0, + layers=[l for l in pad.layers if l.endswith('.Cu')], + free=True, + net=pad.net.number if pad.net else 0, + tstamp=pad.tstamp) + @property def abs_pos(self): return self.at.x, self.at.y, 0, False @@ -282,10 +300,10 @@ class Via: SUPPORTED_FILE_FORMAT_VERSIONS = [20210108, 20211014, 20221018, 20230517] @sexp_type('kicad_pcb') class Board: - _version: Named(int, name='version') = 20210108 + _version: Named(int, name='version') = 20230517 generator: Named(Atom) = Atom.gerbonara - general: GeneralSection = field(default_factory=GeneralSection) - page: PageSettings = field(default_factory=PageSettings) + general: GeneralSection = None + page: PageSettings = None layers: Named(Array(Untagged(LayerSettings))) = field(default_factory=list) setup: BoardSetup = field(default_factory=BoardSetup) properties: List(Property) = field(default_factory=list) @@ -317,6 +335,49 @@ class Board: _trace_index_map: dict = None + @classmethod + def empty_board(kls, inner_layers=0, **kwargs): + if 'setup' not in kwargs: + kwargs['setup'] = None + b = Board(**kwargs) + b.init_default_layers(inner_layers) + b.__after_parse__(None) + return b + + def init_default_layers(self, inner_layers=0): + inner = [(i, f'In{i}.Cu', 'signal', None) for i in range(1, inner_layers+1)] + self.layers = [LayerSettings(idx, name, Atom(ltype)) for idx, name, ltype, cname in [ + (0, 'F.Cu', 'signal', None), + *inner, + (31, 'B.Cu', 'signal', None), + (32, 'B.Adhes', 'user', 'B.Adhesive'), + (33, 'F.Adhes', 'user', 'F.Adhesive'), + (34, 'B.Paste', 'user', None), + (35, 'F.Paste', 'user', None), + (36, 'B.SilkS', 'user', 'B.Silkscreen'), + (37, 'F.SilkS', 'user', 'F.Silkscreen'), + (38, 'B.Mask', 'user', None), + (39, 'F.Mask', 'user', None), + (40, 'Dwgs.User', 'user', 'User.Drawings'), + (41, 'Cmts.User', 'user', 'User.Comments'), + (42, 'Eco1.User', 'user', 'User.Eco1'), + (43, 'Eco2.User', 'user', 'User.Eco2'), + (44, 'Edge.Cuts', 'user', None), + (45, 'Margin', 'user', None), + (46, 'B.CrtYd', 'user', 'B.Courtyard'), + (47, 'F.CrtYd', 'user', 'F.Courtyard'), + (48, 'B.Fab', 'user', None), + (49, 'F.Fab', 'user', None), + (50, 'User.1', 'user', None), + (51, 'User.2', 'user', None), + (52, 'User.3', 'user', None), + (53, 'User.4', 'user', None), + (54, 'User.5', 'user', None), + (55, 'User.6', 'user', None), + (56, 'User.7', 'user', None), + (57, 'User.8', 'user', None), + (58, 'User.9', 'user', None)]] + def rebuild_trace_index(self): idx = self._trace_index = rtree.index.Index() id_map = self._trace_index_map = {} @@ -473,7 +534,7 @@ class Board: net=self.net_id(net_name)) case cad_pr.Via(pad_stack=cad_pr.ThroughViaStack(hole, dia, unit=st_unit)): - x, y, _a, _f = obj.abs_pos() + x, y, _a, _f = obj.abs_pos x, y = MM(x, st_unit), MM(y, obj.unit) yield Via( locked=locked, @@ -484,7 +545,7 @@ class Board: net=self.net_id(net_name)) case cad_pr.Text(_x, _y, text, font_size, stroke_width, h_align, v_align, layer, dark): - x, y, a, flip = obj.abs_pos() + x, y, a, flip = obj.abs_pos x, y = MM(x, st_unit), MM(y, st_unit) size = MM(size, unit) yield gr.Text( diff --git a/twisted_coil_gen_twolayer.py b/twisted_coil_gen_twolayer.py index d84b4db..4284e8a 100644 --- a/twisted_coil_gen_twolayer.py +++ b/twisted_coil_gen_twolayer.py @@ -130,6 +130,7 @@ def print_valid_twists(ctx, param, value): @click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.") @click.option('--layer-pair', default='F.Cu,B.Cu', help="Target KiCad layer pair for the generated footprint, comma-separated. Default: F.Cu/B.Cu.") @click.option('--turns', type=int, default=5, help='Number of turns') +@click.option('--pcb/--footprint', default=False, help='Generate a KiCad PCB instead of a footprint') @click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]') @click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]') @click.option('--trace-width', type=float, default=None) @@ -147,7 +148,7 @@ def print_valid_twists(ctx, param, value): @click.version_option() def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_drill, via_offset, trace_width, clearance, footprint_name, layer_pair, twists, clipboard, counter_clockwise, keepout_zone, keepout_margin, - arc_tolerance): + arc_tolerance, pcb): if 'WAYLAND_DISPLAY' in os.environ: copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip' else: @@ -450,30 +451,40 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d else: zones = [] - fp = kicad_fp.Footprint( - name=name, - generator=kicad_fp.Atom('GerbonaraTwistedCoilGenV1'), - layer='F.Cu', - descr=f"{turns} turn {outer_diameter:.2f} mm diameter twisted coil footprint, inductance approximately {L:.6f} µH. Generated by gerbonara'c Twisted Coil generator, version {__version__}.", - clearance=clearance, - zone_connect=0, - lines=lines, - arcs=arcs, - pads=pads, - zones=zones, - ) + if pcb: + obj = kicad_pcb.Board.empty_board( + zones=zones, + lines=[line.to_graphical_primitive() for line in lines], + arcs=[arc.to_graphical_primitive() for arc in arcs], + vias=[kicad_pcb.Via.from_pad(pad) for pad in pads if pad.type == kicad_pcb.Atom.thru_hole]) + + else: + obj = kicad_fp.Footprint( + name=name, + generator=kicad_fp.Atom('GerbonaraTwistedCoilGenV1'), + layer='F.Cu', + descr=f"{turns} turn {outer_diameter:.2f} mm diameter twisted coil footprint, inductance approximately {L:.6f} µH. Generated by gerbonara'c Twisted Coil generator, version {__version__}.", + clearance=clearance, + zone_connect=0, + lines=lines, + arcs=arcs, + pads=pads, + zones=zones, + ) if clipboard: try: + data = obj.serialize() print(f'Running {copy[0]}.', file=sys.stderr) proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=True) - proc.communicate(fp.serialize()) + proc.communicate(data) + print('passed to wl-clip:', data) except FileNotFoundError: print(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr) elif not outfile: - print(fp.serialize()) + print(obj.serialize()) else: - fp.write(outfile) + obj.write(outfile) if __name__ == '__main__': generate() -- cgit