From 7bfaabc83997fb01490dd3398ac689c3858b3ac3 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 15 Dec 2025 23:08:19 +0100 Subject: [PATCH 1/6] Bump version to v0.9.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9f964ab..82c2c92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kicoil" -version = "0.9.0" +version = "0.10.0" description = "Planar Inductor Generator" readme = "README.rst" license = "Apache-2.0" From 5bdf4d327463cda1e81176d546fbd2baf1ce67f4 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 15 Dec 2025 23:09:07 +0100 Subject: [PATCH 2/6] package.py: Adjust kicad packaging message --- package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.py b/package.py index a11a9da..b20ff43 100644 --- a/package.py +++ b/package.py @@ -111,7 +111,7 @@ def do_release(dry_run): if not dry_run: print('Create git commit') - subprocess.run(['git', 'commit', '-m', f'Version {version}', '--no-edit'], check=True, capture_output=True) + subprocess.run(['git', 'commit', '-m', f'KiCad package version {version}', '--no-edit'], check=True, capture_output=True) res = subprocess.run('git rev-parse --short HEAD'.split(), check=True, capture_output=True, text=True) print(f'Created commit {res.stdout.strip()}') print(f'Creating and signing version tag v{version}') From 3e6c7d6f57ad160bd9badbba26fc36f194f20be1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 16 Dec 2025 13:51:19 +0100 Subject: [PATCH 3/6] WIP --- src/kicoil/geometry.py | 2 ++ src/kicoil/skeletonator.py | 35 +++++++++++++++++++---------------- uv.lock | 2 +- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/kicoil/geometry.py b/src/kicoil/geometry.py index 7d17d55..fedf483 100644 --- a/src/kicoil/geometry.py +++ b/src/kicoil/geometry.py @@ -518,6 +518,7 @@ class PlanarInductor(): fold_angle = start_angle + self.sweeping_angle end_angle = fold_angle + self.sweeping_angle + print(f'### TWIST {i} INWARD ###') # Handle the spiral arm points_layer0, arm_length, angle_refs_layer0 = self.shape.compute_spiral(a1=start_angle, a2=fold_angle, fn=circle_segments) x0, y0 = points_layer0[0] @@ -533,6 +534,7 @@ class PlanarInductor(): footprint.lines.extend(kicad.make_line(*p1, *p2, self.trace_width, self.layer_pair[0]) for p1, p2 in zip(points_layer0, points_layer0[1:])) if self.layers > 1: + print(f'### TWIST {i} OUTWARD ###') # Handle the returning arm on the bottom layer points_layer1, _, angle_refs_layer1 = self.shape.compute_spiral(a1=end_angle, a2=fold_angle, fn=circle_segments) points_layer1 = points_layer1[::-1] diff --git a/src/kicoil/skeletonator.py b/src/kicoil/skeletonator.py index cb56db1..8f42106 100644 --- a/src/kicoil/skeletonator.py +++ b/src/kicoil/skeletonator.py @@ -247,50 +247,53 @@ class Skeletonator: return arcs, points def do_spiral(self, t1, t2, r1=None, r2=None): + print(f' {t1=:.5f} {t2=:.5f} {r1=:.2f} {r2=:.2f}') if r1 is None: r1 = self.radius if r2 is None: r2 = self.min_radius - if t2 < t1: - t1, t2 = t2, t1 - r1, r2 = r2, r1 - def r_interpolate(t): - t = max(t1, min(t2, t)) # Clip to start/end of spiral f = (t - t1) / (t2 - t1) + f = min(1, max(0, f)) # Clip to start/end of spiral return r1 + (r2 - r1) * f - for t_start in range(math.floor(t1), math.ceil(t2)): - t_end = t_start + 1 - r_outer = r_interpolate(t_start) - r_inner = r_interpolate(t_end) - r_ref = min(r_inner, r_outer) # Handle outward spirals where the radii are swapped + turn_range = list(range(math.floor(min(t1, t2)), math.ceil(max(t1, t2))+1)) + if t2 < t1: + turn_range = list(reversed(turn_range)) + print(f' {turn_range=}') + for t_start, t_end in zip(turn_range, turn_range[1:]): + r_start = r_interpolate(t_start) + r_end = r_interpolate(t_end) + r_ref = r_start _ic_arcs, inner_circumference = self.map_circumference(r_ref) + print(f' {r_ref=} {r_start=} {r_end=}') angle = t_start circumference_angles = [] inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference)) - point_angles = [] + point_angles = [t_start] for p1, p2 in edge_cycle(inner_circumference): edge_angle = math.dist(p1, p2) / inner_circumference_sum point_angles.append(angle) angle += edge_angle - point_angles.append(t_end) - for (p1, p2), (tp1, tp2) in zip(self.poly_edges, itertools.pairwise(point_angles)): + edge_angles = list(zip(self.poly_edges, itertools.pairwise(point_angles))) + if t2 < t1: + edge_angles = reversed(edge_angles) + for (p1, p2), (tp1, tp2) in edge_angles: rp1 = r_interpolate(tp1) rp2 = r_interpolate(tp2) _arc, p1_proj = self.project_arc(p1, rp1) _arc, p2_proj = self.project_arc(p2, rp2) - if approx_in_range(t1, tp1, tp2): + if approx_in_range(t1, min(tp1, tp2), max(tp1, tp2)): _arc, p2_proj_r1 = self.project_arc(p2, r1) yield interpolate(p1_proj, p2_proj_r1, t1, tp1, tp2), r_ref - if approx_in_range(t2, tp1, tp2): + if approx_in_range(t2, min(tp1, tp2), max(tp1, tp2)): _arc, p1_proj_r2 = self.project_arc(p1, r2) yield interpolate(p1_proj_r2, p2_proj, t2, tp1, tp2), r_ref - elif approx_in_range(tp2, t1, t2): + elif approx_in_range(tp2, min(t1, t2), max(t1, t2)): yield p2_proj, r_ref def dump_to_pdf(self, filename): diff --git a/uv.lock b/uv.lock index c28bbe4..c2048c5 100644 --- a/uv.lock +++ b/uv.lock @@ -569,7 +569,7 @@ wheels = [ [[package]] name = "kicoil" -version = "0.9.0" +version = "0.10.0" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, From 556707dc35cdba0ea774892aaf99389a29651984 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 16 Dec 2025 14:26:45 +0100 Subject: [PATCH 4/6] WIP --- src/kicoil/skeletonator.py | 77 +++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/src/kicoil/skeletonator.py b/src/kicoil/skeletonator.py index 8f42106..68fdb93 100644 --- a/src/kicoil/skeletonator.py +++ b/src/kicoil/skeletonator.py @@ -253,54 +253,79 @@ class Skeletonator: if r2 is None: r2 = self.min_radius + if t2 < t1: + t1, t2 = t2, t1 + r1, r2 = r2, r1 + def r_interpolate(t): f = (t - t1) / (t2 - t1) f = min(1, max(0, f)) # Clip to start/end of spiral return r1 + (r2 - r1) * f - turn_range = list(range(math.floor(min(t1, t2)), math.ceil(max(t1, t2))+1)) - if t2 < t1: - turn_range = list(reversed(turn_range)) - print(f' {turn_range=}') - for t_start, t_end in zip(turn_range, turn_range[1:]): - r_start = r_interpolate(t_start) - r_end = r_interpolate(t_end) - r_ref = r_start - _ic_arcs, inner_circumference = self.map_circumference(r_ref) - print(f' {r_ref=} {r_start=} {r_end=}') + r_ref = min(r1, r2) # r_start, r_end) # Handle outward spirals where the radii are swapped + _ic_arcs, inner_circumference = self.map_circumference(r_ref) + inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference)) - angle = t_start - circumference_angles = [] - inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference)) - point_angles = [t_start] - for p1, p2 in edge_cycle(inner_circumference): - edge_angle = math.dist(p1, p2) / inner_circumference_sum - point_angles.append(angle) - angle += edge_angle + angle = 0 + point_angles = [0] + for p1, p2 in edge_cycle(inner_circumference): + edge_angle = math.dist(p1, p2) / inner_circumference_sum + point_angles.append(angle) + angle += edge_angle - edge_angles = list(zip(self.poly_edges, itertools.pairwise(point_angles))) - if t2 < t1: - edge_angles = reversed(edge_angles) - for (p1, p2), (tp1, tp2) in edge_angles: + _oc_arcs, outer_circumference = self.map_circumference(max(r1, r2)) + outer_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(outer_circumference)) + + angle = 0 + point_angles_outer = [0] + for p1, p2 in edge_cycle(outer_circumference): + edge_angle = math.dist(p1, p2) / outer_circumference_sum + point_angles_outer.append(angle) + angle += edge_angle + + for ia1, ia2, oa1, oa2 in zip(point_angles, point_angles[1:] + [1], point_angles_outer, point_angles_outer[1:] + [1]): + t_map = t1 if r1 > r2 else t2 + t_map_int = math.floor(t_map) + t_map %= 1.0 + + if approx_in_range(t_map, oa1, oa2): + if oa1 == oa2: + t_mapped = ia1 + else: + t_mapped = ia1 + (ia2 - ia1) * ((t_map - oa1) / (oa2 - oa1)) + + #if r1 > r2: + #t1 = t_mapped + t_map_int + #else: + #t2 = t_mapped + t_map_int + print(f'mapped {t_map=:.3f} to {t_mapped=:.3f}') + break + + turn_angles = range(math.floor(t1), math.ceil(t2) + 1) + for t_start, t_end in zip(turn_angles, turn_angles[1:]): + t_end = t_start + 1 + + for (p1, p2), (tp1, tp2) in zip(self.poly_edges, itertools.pairwise(point_angles)): + tp1, tp2 = tp1 + t_start, tp2 + t_start rp1 = r_interpolate(tp1) rp2 = r_interpolate(tp2) _arc, p1_proj = self.project_arc(p1, rp1) _arc, p2_proj = self.project_arc(p2, rp2) - if approx_in_range(t1, min(tp1, tp2), max(tp1, tp2)): + if approx_in_range(t1, tp1, tp2): _arc, p2_proj_r1 = self.project_arc(p2, r1) yield interpolate(p1_proj, p2_proj_r1, t1, tp1, tp2), r_ref - if approx_in_range(t2, min(tp1, tp2), max(tp1, tp2)): + if approx_in_range(t2, tp1, tp2): _arc, p1_proj_r2 = self.project_arc(p1, r2) yield interpolate(p1_proj_r2, p2_proj, t2, tp1, tp2), r_ref - elif approx_in_range(tp2, min(t1, t2), max(t1, t2)): + elif approx_in_range(tp2, t1, t2): yield p2_proj, r_ref def dump_to_pdf(self, filename): with PdfPages(filename) as pdf: fig, ax = plt.subplots(figsize=(10, 10)) - # polygon outline + # polygon outliner poly_x = [p[0] for p in self.poly] + [self.poly[0][0]] poly_y = [p[1] for p in self.poly] + [self.poly[0][1]] ax.plot(poly_x, poly_y, 'b-', linewidth=2, label='Polygon') From 87033c116fa85a7e3194fea5c09c2fc37cf6f4a6 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 16 Dec 2025 15:28:50 +0100 Subject: [PATCH 5/6] Improve spiral layout for round-ish shapes --- src/kicoil/geometry.py | 10 +++++- src/kicoil/skeletonator.py | 66 ++++++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/kicoil/geometry.py b/src/kicoil/geometry.py index fedf483..c092f47 100644 --- a/src/kicoil/geometry.py +++ b/src/kicoil/geometry.py @@ -142,6 +142,8 @@ class CircleShape(Shape): def project_point(self, r, a, r_ref=None): return cos(a) * r, sin(a) * r + def map_angle(self, a): + return a def offset_exterior(self, margin): r = self.outer_radius + margin @@ -180,6 +182,12 @@ class OffsetShape(Shape): return points, arm_length, angle_refs + def map_angle(self, a): + a_new = self.sk.map_angle(a / (2*pi), self.outer_radius, self.inner_radius) + print(f'NEW MAPPED {a:.3f} to {a_new:.3f}') + return a_new * 2 * pi + + def project_point(self, r, a, r_ref=None): # Skeletonator uses a t coordinate from 0 - 1 per revolution instead of a radian angle. return self.sk.project_point(a/(2*pi) % 1, r, r_ref=r_ref) @@ -588,7 +596,7 @@ class PlanarInductor(): points_layer0, refs_layer0 = arms_layers[0][i] points_layer1, refs_layer1 = arms_layers[1][(i - self.turns) % self.twists] - xv, yv = self.shape.project_point(r, start_angle, r_ref=refs_layer0[0]) + xv, yv = self.shape.project_point(r, self.shape.map_angle(start_angle), r_ref=refs_layer0[0]) footprint.lines.append(kicad.make_line(*points_layer0[0], xv, yv, self.trace_width, self.layer_pair[0])) footprint.lines.append(kicad.make_line(*points_layer1[-1], xv, yv, self.trace_width, self.layer_pair[1])) diff --git a/src/kicoil/skeletonator.py b/src/kicoil/skeletonator.py index 68fdb93..b4701da 100644 --- a/src/kicoil/skeletonator.py +++ b/src/kicoil/skeletonator.py @@ -245,6 +245,39 @@ class Skeletonator: arcs.append(arc) points.append(pt) return arcs, points + + def map_angle(self, t, r1, r2): + r_ref = min(r1, r2) + _ic_arcs, inner_circumference = self.map_circumference(r_ref) + inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference)) + + angle = 0 + point_angles = [0] + for p1, p2 in edge_cycle(inner_circumference): + edge_angle = math.dist(p1, p2) / inner_circumference_sum + angle += edge_angle + point_angles.append(angle) + + _oc_arcs, outer_circumference = self.map_circumference(max(r1, r2)) + outer_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(outer_circumference)) + + angle = 0 + point_angles_outer = [0] + for p1, p2 in edge_cycle(outer_circumference): + edge_angle = math.dist(p1, p2) / outer_circumference_sum + angle += edge_angle + point_angles_outer.append(angle) + + t_map_int = math.floor(t) + t %= 1.0 + + for ia1, ia2, oa1, oa2 in zip(point_angles, point_angles[1:] + [1], point_angles_outer, point_angles_outer[1:] + [1]): + + if approx_in_range(t, oa1, oa2): + if oa1 == oa2: + return t_map_int + ia2 + else: + return t_map_int + ia1 + (ia2 - ia1) * ((t - oa1) / (oa2 - oa1)) def do_spiral(self, t1, t2, r1=None, r2=None): print(f' {t1=:.5f} {t2=:.5f} {r1=:.2f} {r2=:.2f}') @@ -270,36 +303,13 @@ class Skeletonator: point_angles = [0] for p1, p2 in edge_cycle(inner_circumference): edge_angle = math.dist(p1, p2) / inner_circumference_sum + angle += edge_angle point_angles.append(angle) - angle += edge_angle - _oc_arcs, outer_circumference = self.map_circumference(max(r1, r2)) - outer_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(outer_circumference)) - - angle = 0 - point_angles_outer = [0] - for p1, p2 in edge_cycle(outer_circumference): - edge_angle = math.dist(p1, p2) / outer_circumference_sum - point_angles_outer.append(angle) - angle += edge_angle - - for ia1, ia2, oa1, oa2 in zip(point_angles, point_angles[1:] + [1], point_angles_outer, point_angles_outer[1:] + [1]): - t_map = t1 if r1 > r2 else t2 - t_map_int = math.floor(t_map) - t_map %= 1.0 - - if approx_in_range(t_map, oa1, oa2): - if oa1 == oa2: - t_mapped = ia1 - else: - t_mapped = ia1 + (ia2 - ia1) * ((t_map - oa1) / (oa2 - oa1)) - - #if r1 > r2: - #t1 = t_mapped + t_map_int - #else: - #t2 = t_mapped + t_map_int - print(f'mapped {t_map=:.3f} to {t_mapped=:.3f}') - break + if r1 > r2: + t1 = self.map_angle(t1, r1, r2) + else: + t2 = self.map_angle(t2, r1, r2) turn_angles = range(math.floor(t1), math.ceil(t2) + 1) for t_start, t_end in zip(turn_angles, turn_angles[1:]): From 82eccbad1d4b7388c39a9751fc20ec3dc7d246bf Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 17 Dec 2025 10:22:19 +0100 Subject: [PATCH 6/6] Alternative approach works but looks bad --- src/kicoil/geometry.py | 4 +-- src/kicoil/skeletonator.py | 71 ++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/kicoil/geometry.py b/src/kicoil/geometry.py index c092f47..dbabb1d 100644 --- a/src/kicoil/geometry.py +++ b/src/kicoil/geometry.py @@ -578,8 +578,8 @@ class PlanarInductor(): xv, yv = self.shape.project_point(r, fold_angle, r_ref=refs_layer0[-1]) - footprint.lines.append(kicad.make_line(*points_layer0[-1], xv, yv, self.trace_width, self.layer_pair[0])) - footprint.lines.append(kicad.make_line(xv, yv, *points_layer1[0], self.trace_width, self.layer_pair[1])) + #footprint.lines.append(kicad.make_line(*points_layer0[-1], xv, yv, self.trace_width, self.layer_pair[0])) + #footprint.lines.append(kicad.make_line(xv, yv, *points_layer1[0], self.trace_width, self.layer_pair[1])) footprint.pads.append(kicad.make_via(xv, yv, self.via_diameter, self.via_drill, self.clearance, diff --git a/src/kicoil/skeletonator.py b/src/kicoil/skeletonator.py index b4701da..2ee8ee1 100644 --- a/src/kicoil/skeletonator.py +++ b/src/kicoil/skeletonator.py @@ -31,6 +31,14 @@ def interpolate(p1, p2, t, t_start=0, t_end=1): return (x1 + t*dx, y1 + t*dy) +def interpolate_1d(a, b, t, t_start=0, t_end=1): + if math.isclose(t_start, t_end): + return a + t_range = t_end - t_start + t = (t - t_start) / t_range + return a + (b-a) * t + + def approx_in_range(value, lower, upper): """ Approximate range check """ if math.isclose(value, lower) or math.isclose(value, upper): @@ -278,7 +286,8 @@ class Skeletonator: return t_map_int + ia2 else: return t_map_int + ia1 + (ia2 - ia1) * ((t - oa1) / (oa2 - oa1)) - + + def do_spiral(self, t1, t2, r1=None, r2=None): print(f' {t1=:.5f} {t2=:.5f} {r1=:.2f} {r2=:.2f}') if r1 is None: @@ -290,46 +299,32 @@ class Skeletonator: t1, t2 = t2, t1 r1, r2 = r2, r1 - def r_interpolate(t): - f = (t - t1) / (t2 - t1) - f = min(1, max(0, f)) # Clip to start/end of spiral - return r1 + (r2 - r1) * f + angle_map = [] + circumferences = [] + n = 100 + radius_steps = [r1 + (r2 - r1) * i/(n-1) for i in range(n)] + angle_steps = [t1 + (t2 - t1) * i/(n-1) for i in range(n)] + for r in radius_steps: + _ic_arcs, circumference = self.map_circumference(r) + circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(circumference)) + circumferences.append(circumference_sum) - r_ref = min(r1, r2) # r_start, r_end) # Handle outward spirals where the radii are swapped - _ic_arcs, inner_circumference = self.map_circumference(r_ref) - inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference)) + angle = 0 + point_angles = [0] + for p1, p2 in edge_cycle(circumference): + edge_angle = math.dist(p1, p2) / circumference_sum + angle += edge_angle + point_angles.append(angle) + + angle_map.append(point_angles) - angle = 0 - point_angles = [0] - for p1, p2 in edge_cycle(inner_circumference): - edge_angle = math.dist(p1, p2) / inner_circumference_sum - angle += edge_angle - point_angles.append(angle) - - if r1 > r2: - t1 = self.map_angle(t1, r1, r2) - else: - t2 = self.map_angle(t2, r1, r2) - - turn_angles = range(math.floor(t1), math.ceil(t2) + 1) - for t_start, t_end in zip(turn_angles, turn_angles[1:]): - t_end = t_start + 1 - - for (p1, p2), (tp1, tp2) in zip(self.poly_edges, itertools.pairwise(point_angles)): - tp1, tp2 = tp1 + t_start, tp2 + t_start - rp1 = r_interpolate(tp1) - rp2 = r_interpolate(tp2) - _arc, p1_proj = self.project_arc(p1, rp1) - _arc, p2_proj = self.project_arc(p2, rp2) + for r, t, point_angles in zip(radius_steps, angle_steps, angle_map): + for (p1, p2), (tp1, tp2) in zip(self.poly_edges, itertools.pairwise(point_angles + point_angles[:1])): + _arc, p1_proj = self.project_arc(p1, r) + _arc, p2_proj = self.project_arc(p2, r) - if approx_in_range(t1, tp1, tp2): - _arc, p2_proj_r1 = self.project_arc(p2, r1) - yield interpolate(p1_proj, p2_proj_r1, t1, tp1, tp2), r_ref - if approx_in_range(t2, tp1, tp2): - _arc, p1_proj_r2 = self.project_arc(p1, r2) - yield interpolate(p1_proj_r2, p2_proj, t2, tp1, tp2), r_ref - elif approx_in_range(tp2, t1, t2): - yield p2_proj, r_ref + if approx_in_range(t%1, tp1, tp2): + yield interpolate(p1_proj, p2_proj, t%1, tp1, tp2), r def dump_to_pdf(self, filename): with PdfPages(filename) as pdf: