From f9a1d61e593802a32b9484d156b950bc0c32bec1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 8 Dec 2025 00:32:42 +0100 Subject: [PATCH] WIP --- src/kicoil/geometry.py | 190 ++++++++++------------------------------- src/kicoil/kicad.py | 7 ++ 2 files changed, 52 insertions(+), 145 deletions(-) diff --git a/src/kicoil/geometry.py b/src/kicoil/geometry.py index a60599a..c222aa7 100644 --- a/src/kicoil/geometry.py +++ b/src/kicoil/geometry.py @@ -58,30 +58,6 @@ def angle_between_vectors(va, vb): return angle -def traces_to_magneticalc(traces, out, pcb_thickness=0.8): - coords = [] - last_x, last_y, last_z = None, None, None - def coord(x, y, z): - nonlocal coords, last_x, last_y, last_z - if (x, y, z) != (last_x, last_y, last_z): - coords.append((x, y, z)) - - render_cache = {} - for tr in traces: - z = pcb_thickness if tr[1].layer == 'F.Cu' else 0 - objs = [obj - for elem in tr - for obj in elem.render(cache=render_cache) - if isinstance(elem, (kicad_pcb.TrackSegment, kicad_pcb.TrackArc))] - - # start / switch layer - coord(objs[0].x1, objs[0].y1, z) - - for ob in objs: - coord(ob.x2, ob.y2, z) - - np.savetxt(out, np.array(coords) / 10) # magneticalc expects centimeters, not millimeters. - # https://en.wikipedia.org/wiki/Farey_sequence#Next_term def farey_sequence(n: int, descending: bool = False) -> None: """Print the n'th Farey sequence. Allow for either ascending or descending.""" @@ -190,7 +166,7 @@ def compute_spiral(r1, r2, a1, a2, start_frac, end_frac, fn=64): @click.option('--show-twists', callback=print_valid_twists, expose_value=False, type=int, is_eager=True, help='Calculate and show valid --twists counts for the given number of turns. Takes the number of turns as a value.') @click.option('--clearance', type=float, default=None) @click.option('--arc-tolerance', type=float, default=0.02) -@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'magneticalc', 'show']), default='kicad-footprint') +@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'show']), default='kicad-footprint') @click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)') @click.option('--counter-clockwise/--clockwise', help='Direction of generated spiral. Default: clockwise when wound from the inside.') @click.version_option() @@ -295,9 +271,20 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d print(f'Fill factor: {phi:g}', file=sys.stderr) print(f'Approximate inductance: {L:g} µH', file=sys.stderr) - pads = [] - lines = [] - arcs = [] + if footprint_name: + name = footprint_name + elif outfile: + name = outfile.stem, + else: + name = f'generated-coil-{outer_diameter:.2f}x{inner_diameter:.2f}-n{turns}-k{twists}' + + footprint = kicad_fp.Footprint( + name=name, + generator=kicad_fp.Atom('KicoilV1'), + 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) sector_angle = 2*pi / twists total_angle = twists*2*sweeping_angle if two_layer else twists*sweeping_angle @@ -306,18 +293,20 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d for i in range(twists): inverse[i*turns%twists] = i - layer_sections = [] + # Array where we collect all gerbonara kicad line and arc objects for i in range(twists): start_angle = i*sector_angle fold_angle = start_angle + sweeping_angle end_angle = fold_angle + sweeping_angle + # Handle the spiral arm x = inverse[i]*floor(2*sweeping_angle / (2*pi)) * 2*pi points_layer0, arm_length = compute_spiral(outer_radius, inner_radius, start_angle, fold_angle, (x + start_angle)/total_angle, (x + fold_angle)/total_angle, circle_segments) x0, y0 = points_layer0[0] xn, yn = points_layer0[-1] if two_layer: + # Handle the returning arm on the bottom layer points_layer1, _ = compute_spiral(inner_radius, outer_radius, fold_angle, end_angle, (x + fold_angle)/total_angle, (x + end_angle)/total_angle, circle_segments) else: @@ -327,167 +316,78 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d yq = yn - sin(fold_angle) * dr points_layer1 = [(xn, yn), (xq, yq)] - #r, g, b, _a = mpl.cm.plasma(start_frac + (end_frac - start_frac)/fn * (i + 0.5)) - #path = SVGPath(fill='none', stroke=f'#{round(r*255):02x}{round(g*255):02x}{round(b*255):02x}', stroke_width=trace_width, stroke_linejoin='round', stroke_linecap='round') - #svg_stuff.append(path) - #path.move(xp, yp) - #path.line(xn, yn) -# lines.append(make_line(xp, yp, xn, yn, layer_pair[layer])) -# if use_arcs: -# arcs.extend(arc_approximate(points, layer_pair[layer], arc_tolerance)) - #svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_diameter/2, stroke='none', fill='white')) - #svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_drill/2, stroke='none', fill='black')) - #pads.append(make_via(xv, yv, layer_pair)) + footprint.arcs.extend(arc_approximate(points_layer0, layer_pair[0], arc_tolerance)) + footprint.arcs.extend(arc_approximate(points_layer1, layer_pair[1], arc_tolerance)) + # Handle inner via ring and process staggering if enabled r = inner_via_ring_radius if stagger_inner_vias: if i%2 != 0: r -= 2*via_offset - xv, yv = r*cos(fold_angle), r*sin(fold_angle) - if not isclose(via_offset, 0, abs_tol=1e-6): - points_layer0.append([xv, yv]) - points_layer1.insert(0, [xv, yv]) + xv, yv = r*cos(fold_angle), r*sin(fold_angle) + + if not isclose(via_offset, 0, abs_tol=1e-6): + footprint.lines.append(make_line(*points_layer0[-1], xv, yv, layer_pair[0])) + footprint.lines.append(make_line(xv, yv, *points_layer1[0], layer_pair[1])) + + # Handle outer via ring and process staggering if enabled unless we are at the start of the coil, where we will + # place pads below. if i > 0: r = outer_via_ring_radius + if stagger_outer_vias: if i%2 != 0: r += 2*via_offset + xv, yv = r*cos(start_angle), r*sin(start_angle) + if not isclose(via_offset, 0, abs_tol=1e-6): - points_layer0.insert(0, [xv, yv]) - points_layer1.insert(0, [xv, yv]) - lines.append(make_line(x0, y0, xv, yv, layer_pair[0])) - lines.append(make_line(x0, y0, xv, yv, layer_pair[1])) - svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_diameter/2, stroke='none', fill='white')) - svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_drill/2, stroke='none', fill='black')) + footprint.lines.append(make_line(x0, y0, xv, yv, layer_pair[0])) + footprint.lines.append(make_line(x0, y0, xv, yv, layer_pair[1])) l_total = arm_length*twists*(2 if two_layer else 1) print(f'Approximate track length: {l_total:.2f} mm', file=sys.stderr) + A = copper_thickness/1e3 * trace_width/1e3 rho = 1.68e-8 R = l_total/1e3 * rho / A print(f'Approximate resistance: {R:g} Ω', file=sys.stderr) + # Place the pads on the outer radius top_pad = make_pad(1, [layer_pair[0]], outer_radius, 0) pads.append(top_pad) bottom_pad = make_pad(2, [layer_pair[1]], outer_radius, 0) pads.append(bottom_pad) - svg_stuff += svg_vias - - svg_stuff.append(Tag('path', d=f'M {inner_radius} 0 L {outer_radius} 0', stroke=rainbow[n+1], fill='none', - stroke_width='0.05mm', stroke_linecap='round')) - ntraces = int(turns_per_layer)+1 - alpha = [0] * ntraces - for i in range(ntraces): - c = inner_radius + (outer_radius-inner_radius) / turns_per_layer * i - #dalpha = dy / c - #dx / dalpha = (outer_radius - inner_radius) / sweeping_angle - #c * (dx / dy) = (outer_radius - inner_radius) / sweeping_angle - #dx / dy = (outer_radius - inner_radius) / sweeping_angle / c - dx = (outer_radius - inner_radius) / sweeping_angle / c - alpha[i] = atan(dx) - dy = 0.3 - dx *= dy - r = trace_width/2 / cos(alpha[i]) - svg_stuff.append(Tag('path', d=f'M {c-r+dx} {-dy} L {c-r-dx} {dy}', stroke=rainbow[n+1], fill='none', - stroke_width='0.05mm', stroke_linecap='round')) - svg_stuff.append(Tag('path', d=f'M {c+r+dx} {-dy} L {c+r-dx} {dy}', stroke=rainbow[n+1], fill='none', - stroke_width='0.05mm', stroke_linecap='round')) - - #print(f'spiral angle {degrees(alpha[i]):.2f}', file=sys.stderr) - - for i, (a1, a2) in enumerate(zip(alpha[::-1], alpha[1::])): - amean = (a2+a1)/2 - pitch = (outer_radius - inner_radius) / turns_per_layer - clearance = pitch - trace_width - clearance *= cos(amean) - - x, y = inner_radius + (i + 1/2)*pitch, -0.5 - svg_stuff.append(Tag('text', - [f'{clearance:.5f}mm'], - x=x, - y=y, - text_anchor='start', - transform=f'rotate(-45 {x} {y})', - style=f'font: 1px bold sans-serif; fill: {rainbow[n+1]}')) - - if svg_out: - svg_file(svg_out, svg_stuff, 100, 100, -50, -50) - - if footprint_name: - name = footprint_name - elif outfile: - name = outfile.stem, - else: - name = 'generated_coil' - if keepout_zone: r = outer_diameter/2 + keepout_margin tol = 0.05 # mm n = ceil(pi / acos(1 - tol/r)) pts = [(r*cos(a*2*pi/n), r*sin(a*2*pi/n)) for a in range(n)] - zones = [kicad_pr.Zone(layers=['*.Cu'], + footprint.zones.append(kicad_pr.Zone(layers=['*.Cu'], hatch=kicad_pr.Hatch(), filled_areas_thickness=False, keepout=kicad_pr.ZoneKeepout(copperpour_allowed=False), - polygon=kicad_pr.ZonePolygon(pts=[kicad_pr.XYCoord(x=x, y=y) for x, y in pts]))] - else: - zones = [] + polygon=kicad_pr.ZonePolygon(pts=[kicad_pr.XYCoord(x=x, y=y) for x, y in pts]))) - if pcb: - obj = kicad_pcb.Board.empty_board( - zones=zones, - track_segments=[kicad_pcb.TrackSegment.from_footprint_line(line) for line in lines], - vias=[kicad_pcb.Via.from_pad(pad) for pad in pads if pad.type == kicad_pcb.Atom.thru_hole]) - obj.rebuild_trace_index() - seg = obj.track_segments[-1] - traces = [] - end = top_pad - layer = 'F.Cu' - while True: - tr = list(obj.find_connected_traces(end, layers=[layer])) - traces.append(tr) - if not isinstance(tr[-1], kicad_pcb.Via): - break - layer = 'B.Cu' if layer == 'F.Cu' else 'F.Cu' - end = tr[-1] - # remove start pad - traces[0] = traces[0][1:] - - r = outer_diameter/2 + 20 - - if magneticalc_out: - traces_to_magneticalc(traces, magneticalc_out) - - 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 format == 'kicad-footprint': + data = footprint.serialize() + elif format == 'kicad-pcb': + data = footprint_to_board(footprint).serialize() + elif format == 'gerber': 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(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(obj.serialize()) + print(data) else: - obj.write(outfile) + outfile.write_text(data) if __name__ == '__main__': generate() diff --git a/src/kicoil/kicad.py b/src/kicoil/kicad.py index b808c78..897026c 100644 --- a/src/kicoil/kicad.py +++ b/src/kicoil/kicad.py @@ -39,3 +39,10 @@ def make_via(x, y, layers): clearance=clearance, zone_connect=0) + +def footprint_to_board(footprint): + return kicad_pcb.Board.empty_board( + zones=zones, + track_segments=[kicad_pcb.TrackSegment.from_footprint_line(line) for line in lines], + vias=[kicad_pcb.Via.from_pad(pad) for pad in pads if pad.type == kicad_pcb.Atom.thru_hole]) +