From d7dd0e363bc62ea61920435611c89733d94f367e Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 15 Dec 2025 22:28:14 +0100 Subject: [PATCH] Add GDSII and OASIS export --- pyproject.toml | 1 + src/kicoil/cli.py | 73 +++++++++++++++++++++++++++++++++++++++++++++-- uv.lock | 36 +++++++++++++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7752eb3..9f964ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dev = [ "ipykernel>=7.1.0", ] gui = ["cairosvg", "pillow"] +gds = ["gdstk"] [build-system] requires = ["uv-build"] diff --git a/src/kicoil/cli.py b/src/kicoil/cli.py index c5ec206..c1f8316 100644 --- a/src/kicoil/cli.py +++ b/src/kicoil/cli.py @@ -45,6 +45,27 @@ def print_valid_twists(ctx, param, value): ctx.exit() +def circle_center_to_tangents(center, a, b): + """ Given two points on a circle and the center of the circle, calculate the intersection of two tangents at the two points """ + cx, cy = center + ax, ay = a + bx, by = b + + dax = ax - cx + day = ay - cy + dbx = bx - cx + dby = by - cy + + v = dax*ax + day*ay + w = dbx*bx + dby*by + det = dax*dby - day*dbx + + ix = (v*dby - day*w) / det + iy = (dax*w - v*dbx) / det + + return ix, iy + + @click.group() @click.option('--turns', type=int, default=5, help='Number of turns') @click.option('--twists', type=int, default=1, help='Number of twists per revolution. Note that this number must be co-prime to the number of turns. Run with --show-twists to list valid values. (default: 1)') @@ -64,19 +85,20 @@ def print_valid_twists(ctx, param, value): @click.option('--circle-segments', type=int, default=64, help='When not using arcs, the number of points to use for arc interpolation per 360 degrees.') @click.option('--arc-tolerance', type=float, default=0.02) @click.option('--approximate-arcs/--no-approximate-arcs', default=True, help='Use circular arcs to smoothen output shape (default: on)') -@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'json', 'show']), default='kicad-footprint') +@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'json', 'gdsii', 'oasis', 'show']), default='kicad-footprint') @click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)') @click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.") +@click.option('--cell-name', help="Name for the generated cell when exporting GDSII. 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.version_option() @click.pass_context -def cli(ctx, footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, **kwargs): +def cli(ctx, footprint_name, cell_name, clipboard, single_layer, arc_tolerance, circle_segments, format, **kwargs): ctx.ensure_object(dict) logger = logging.getLogger('kicoil') logger.setLevel(logging.INFO) def write(shape, outfile): - nonlocal footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format + nonlocal footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, cell_name logger = logging.getLogger('kicoil') if single_layer: @@ -160,6 +182,51 @@ def cli(ctx, footprint_name, clipboard, single_layer, arc_tolerance, circle_segm data = json.dumps(d, indent=4) + elif format in ('gdsii', 'oasis'): + import gdstk + + DRILL_LAYER = 2 + lib = gdstk.Library() + + if cell_name is None: + if outfile: + cell_name = outfile.stem + else: + cell_name = f'planar_coil' + cell = lib.new_cell(cell_name) + + for line in footprint.lines: + layer = model.layer_pair.index(line.layer) + path = gdstk.FlexPath([(line.start.x, line.start.y), (line.end.x, line.end.y)], line.stroke.width, ends=['round'], layer=layer) + cell.add(path) + + for arc in footprint.arcs: + layer = model.layer_pair.index(arc.layer) + center, r, _direction = kicad_mid_to_center_arc(arc.mid, arc.start, arc.end) + proj_x, proj_y = circle_center_to_tangents(center, tuple(arc.start), tuple(arc.end)) + # multiply r with 0.99 to make sure gdstk's interpolation routine catches on since the arc endpoints are calculated exactly + path = gdstk.FlexPath([(arc.start.x, arc.start.y), (proj_x, proj_y), (arc.end.x, arc.end.y)], arc.stroke.width, bend_radius=r*0.99, ends=['round'], layer=layer) + cell.add(path) + + for pad in footprint.pads: + for layer in pad.layers: + layer = model.layer_pair.index(layer) + layer_obj = gdstk.ellipse(tuple(pad.at), pad.size.x/2, layer=layer) + cell.add(layer_obj) + + if pad.drill: + drill = gdstk.ellipse(tuple(pad.at), pad.drill.diameter/2, layer=DRILL_LAYER) + cell.add(drill) + + if clipboard or not outfile: + raise click.ClickException('outfile is required for GDSII or OASIS export') + + if format == 'gdsii': + lib.write_gds(outfile) + else: + lib.write_oas(outfile) + return + elif format in ('svg', 'show'): data = str(make_transparent_svg(footprint)) diff --git a/uv.lock b/uv.lock index 424a655..c28bbe4 100644 --- a/uv.lock +++ b/uv.lock @@ -325,6 +325,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" }, ] +[[package]] +name = "gdstk" +version = "0.9.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/b5/a12ef182943856ffd55a36e825d01f12414ac2f3610950378a69291edf3e/gdstk-0.9.61.tar.gz", hash = "sha256:2967935fdf455c56ca77ad5c703c87cb88644ab75e752dcac866a36499879c6f", size = 317766, upload-time = "2025-08-28T10:17:15.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/9a/7ea7b7a295e029542d4aeb252d01c1bcc8e724df26155f9b5f432b02d02a/gdstk-0.9.61-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3beeae846fc523c7e3a01c47edcd3b7dd83c29650e56b82a371e528f9cb0ec3e", size = 923024, upload-time = "2025-08-28T10:16:53.613Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3d/5aa9b1a4665259702e9f17e03a9a114a873df25c9ba2c9e782ff25fb11e9/gdstk-0.9.61-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:575a21639b31e2fab4d9e918468b8b40a58183028db563e5963be594bff1403d", size = 475687, upload-time = "2025-08-28T10:16:54.886Z" }, + { url = "https://files.pythonhosted.org/packages/80/13/ec783d8de5d9b4e51763102cac6da124f16747e4c73f166c36a105065008/gdstk-0.9.61-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:90d54b48223dcbb8257769faaa87542d12a749d8486e8d1187a45d06e9422859", size = 536872, upload-time = "2025-10-20T11:25:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/4a/365ca49b76ee3d70a0d044fcc44272c85754fd763e0b3f3e9f498b8bf4a1/gdstk-0.9.61-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35405bed95542a0b10f343b165ce0ad80740bf8127a4507565ec74222e6ec8d3", size = 600630, upload-time = "2025-08-28T10:16:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/62/3e/815fd2977ff1d885ad87d0a54deb19926fd025933325c7a27625c1c0a0c3/gdstk-0.9.61-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b311ddf8982995b52ac3bf3b32a6cf6d918afc4e66dea527d531e8af73896231", size = 536873, upload-time = "2025-08-28T10:16:57.516Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8b/1ba0abc4fb3c60015d23894aa9d093f473fdc337584f9c1d7afe96d6f9f5/gdstk-0.9.61-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dcbfc60fba92d10f1c7d612b5409c343fcaf2a380640e9fb01c504ca948b412", size = 540029, upload-time = "2025-10-20T11:26:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/72/cc46f132741e541995ede7fccf9820f105fb2296ab70192bd27de56190f2/gdstk-0.9.61-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:fab67ccdd8029ef7eb873f8c98f875dc2665a5e45af7cf3d2a7a0f401826a1d3", size = 535763, upload-time = "2025-08-28T10:16:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/72196721fedfc38cf21158e5a436d73b41ea244b78c1053462a35d1e42cb/gdstk-0.9.61-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5852749e203d6978e06d02f8ef9e29ce4512cb1aedeb62c37b8e8b2c10c4f529", size = 1711669, upload-time = "2025-08-28T10:16:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/36/ec/eeebcc95c2741e1f39da08c95fdcc56b3d0f5305ad732d8a66213b6fa0b8/gdstk-0.9.61-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ee38a54c799e77dbe219266f765bbd3b2906b62bc7b6fb64b1387e6db3dd187", size = 1535165, upload-time = "2025-08-28T10:17:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a8/653335b1ec13306b023a9aa1e2072e8bab5c0a5376c138066006504198c3/gdstk-0.9.61-cp313-cp313-win_amd64.whl", hash = "sha256:6abb396873b2660dd7863d664b3822f00547bf7f216af27be9f1f812bc5e8027", size = 500117, upload-time = "2025-08-28T10:17:02.459Z" }, + { url = "https://files.pythonhosted.org/packages/33/52/28cd8720357d6b892ac19684e4af57f7264ba8eea0ea8078e17f0476408c/gdstk-0.9.61-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a674af8be5cf1f8ea9f6c5b5f165f797d7e2ed74cbca68b4a22adb92b515fb35", size = 916091, upload-time = "2025-10-20T11:25:35.497Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f1/c55c7b7b0158a8540716a4fea3aaf0288726122afc0e9af1f0564b79605b/gdstk-0.9.61-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:38ec0b7285d6c9bf8cbc279731dc0d314633cda2ce9e6f9053554b3e5f004fcd", size = 474548, upload-time = "2025-10-20T11:25:38.086Z" }, + { url = "https://files.pythonhosted.org/packages/ab/83/23907af9b349d79af54c4a79ac7972b9a52f8cee48b366ee9635cf9a32d9/gdstk-0.9.61-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3b63a77b57fb441c8017217aaf1e8b13d93cbee822031a8e2826adb716e01dd4", size = 537072, upload-time = "2025-10-20T11:25:55.473Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e7/e930928f9a03b896f51b6e975dbb652cffa81c61338609d6289c3f48313c/gdstk-0.9.61-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7fae6eee627e837d1405b47d381ccd33dbba85473b1bb3822bdc8ae41dbc0dc", size = 540132, upload-time = "2025-10-20T11:26:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9d/56afbb84cccb07751f8532591bfc3a1608833943446b51978b52daaaa2b1/gdstk-0.9.61-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9e396694cac24bd87d0e38c37e6740d9ba0c13f6c9f2211a871d62288430f069", size = 1578033, upload-time = "2025-10-20T11:26:18.707Z" }, + { url = "https://files.pythonhosted.org/packages/58/14/c854d3ef2b0ece30c0cb4c034d3b7f58425086383a229c960d813a163861/gdstk-0.9.61-cp314-cp314-win_amd64.whl", hash = "sha256:7ea0c1200dc53b794e9c0cc6fe3ea51e49113dfdd9c3109e1961cda3cc2197c7", size = 513072, upload-time = "2025-10-20T11:25:21.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/0a/5e9c3be1327d36556a5e8d16c6696614fdc1df0d28c0629b829785245d76/gdstk-0.9.61-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:616dd1c3e7aea4a98aeb03db7cf76a853d134c54690790eaa25c63eede7b869a", size = 925091, upload-time = "2025-10-20T11:25:40.772Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a72de67ea1c217537ae537d7e96b6a0b3c7427177bd1439c89e7617ad3f0/gdstk-0.9.61-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b0e898202fbb7fd4c39f8404831415a0aa0445656342102c4e77d4a7c2c15a1d", size = 477723, upload-time = "2025-10-20T11:25:44.535Z" }, + { url = "https://files.pythonhosted.org/packages/0c/94/5034a646ee3bda58210cd377b241582161c8683489210cba762d4f7f06d1/gdstk-0.9.61-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:29bb862a1a814f5bbd6f8bbc2f99e1163df9e6307071cb6e11251dbe7542feb5", size = 541773, upload-time = "2025-10-20T11:25:57.407Z" }, + { url = "https://files.pythonhosted.org/packages/bc/90/3e5883b528dcc7c4daa74925276a09ff274004518deea48c859b9215120b/gdstk-0.9.61-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6c2a08d82a683aff50dc63f2943ed805d32d46bd984cbd4ac9cf876146d0ef9", size = 544525, upload-time = "2025-10-20T11:26:13.877Z" }, + { url = "https://files.pythonhosted.org/packages/46/0b/549a0e72982b87011013705cdb552e2acfdb882ceaeb41a3fe020e981ec8/gdstk-0.9.61-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3ba52f95763052a6968583942e6531ceca20c14c762d44fe2bd887445e2f73b6", size = 1582069, upload-time = "2025-10-20T11:26:21.189Z" }, +] + [[package]] name = "gerbonara" version = "1.6.0" @@ -554,6 +586,9 @@ dependencies = [ dev = [ { name = "ipykernel" }, ] +gds = [ + { name = "gdstk" }, +] gui = [ { name = "cairosvg" }, { name = "pillow" }, @@ -573,6 +608,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [{ name = "ipykernel", specifier = ">=7.1.0" }] +gds = [{ name = "gdstk" }] gui = [ { name = "cairosvg" }, { name = "pillow" },