Compare commits

...
Sign in to create a new pull request.

19 commits

Author SHA1 Message Date
jaseg
7fe1f457dd Update kicad addon metadata description 2025-12-18 16:59:06 +01:00
jaseg
800d705e02 KiCad package version 0.11.1 2025-12-18 16:52:38 +01:00
jaseg
eed50a097e Bump version to v0.11.1 2025-12-18 16:52:23 +01:00
jaseg
c712473064 Fix missing vertex bug when turns align with start 2025-12-18 16:51:41 +01:00
jaseg
67eb82fab5 KiCad package version 0.11.0 2025-12-18 00:04:43 +01:00
jaseg
c04ae73986 Fix packaging script to handle WASM module 2025-12-18 00:04:35 +01:00
jaseg
5893ef234e Bump version to v0.11.0 2025-12-18 00:02:44 +01:00
jaseg
7cac7b1041 Improve GUI parameter validation 2025-12-17 23:57:11 +01:00
jaseg
df2e0c7bcf Add new shapes to GUI 2025-12-17 23:41:11 +01:00
jaseg
6666e665e2 Improve CLI 2025-12-17 23:01:40 +01:00
jaseg
1140c5bca3 Add matplotlib to dev dependencies 2025-12-17 12:48:16 +01:00
jaseg
2b64ee5081 Do not require matplotlib unless debug output is requested 2025-12-17 12:47:39 +01:00
jaseg
e54517544a Remove debug prints, add debug output 2025-12-17 12:47:06 +01:00
jaseg
57e8a52e1f Fix skipping issue 2025-12-17 12:37:41 +01:00
jaseg
b25b32e98f Fix remaining reflection issue 2025-12-17 12:30:04 +01:00
jaseg
96d06a8dc3 Fix multiturn layout broken in last commit 2025-12-17 12:25:48 +01:00
jaseg
b0b942431e Fix rollover problem 2025-12-17 12:18:28 +01:00
jaseg
82d107fc82 package.py: Adjust kicad packaging message 2025-12-17 10:36:37 +01:00
jaseg
4295b2d9f6 Bump version to v0.10.0 2025-12-17 10:36:34 +01:00
11 changed files with 550 additions and 110 deletions

View file

@ -52,7 +52,8 @@ int main()
}
if (!poly.is_counterclockwise_oriented()) {
poly.reverse_orientation();
std::cerr << "Error: Polygon must be counter-clockwise" << std::endl;
return EXIT_FAILURE;
}
SsPtr ss = CGAL::create_interior_straight_skeleton_2(poly.vertices_begin(), poly.vertices_end(), K());

BIN
de.jaseg.kicoil-v0.11.0.zip Normal file

Binary file not shown.

BIN
de.jaseg.kicoil-v0.11.1.zip Normal file

Binary file not shown.

View file

@ -1,8 +1,8 @@
{
"$schema": "https://go.kicad.org/pcm/schemas/v1",
"name": "KiCoil",
"description": "Planar inductor supporting spiral coils, toroidal coils, and hybrids",
"description_full": "KiCoil generates planar inductors as footprints. Currently, circular spiral and toroid inductors are supported. KiCoil supports arbitrary intermediates between spiral and toroid inductors. By playing around with this, you can create inductors that have lower parasitics and higher self-resonant frequency than standard multilayer spiral inductors.",
"description": "Planar inductor supporting arbitrary shapes of spiral coils, toroidal coils, and hybrids",
"description_full": "KiCoil generates planar inductors as footprints. It supports arbitrary shapes with presets such as circles, rectangles, circular sectors, and SVG import for arbitrary shapes. It can generate spiral and toroid inductors as well as arbitrary intermediates between spiral and toroid inductors. By playing around with this, you can create inductors that have lower parasitics and higher self-resonant frequency than standard multilayer spiral inductors.",
"identifier": "de.jaseg.kicoil",
"type": "plugin",
"author": {
@ -26,7 +26,25 @@
"download_size": 38942,
"download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0",
"install_size": 119659
},
{
"version": "0.11.0",
"status": "stable",
"kicad_version": "9.00",
"download_sha256": "455879f51a288ffc399f62556b1d83dc4988349f1759ddb283d711f72406310a",
"download_size": 1886869,
"download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.11.0.zip?h=v0.11.0",
"install_size": 14353567
},
{
"version": "0.11.1",
"status": "stable",
"kicad_version": "9.00",
"download_sha256": "8494927da4d4aca48baf13d6c8a162dc910c0be1294e5a44517b7d27a086a140",
"download_size": 1886906,
"download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.11.1.zip?h=v0.11.1",
"install_size": 14353726
}
],
"runtime": "ipc"
}
}

View file

@ -79,16 +79,18 @@ def do_release(dry_run):
for path in files:
path = root / path
out_path = plugin_dir / path.relative_to(module_sources)
content = path.read_text()
if path.name == '__init__.py':
content = path.read_text()
lines = content.splitlines()
lines_out = []
for line in lines:
if line.startswith('__version__ = version('):
line = f'__version__ = {version!r}'
lines_out.append(line)
content = '\n'.join(lines_out)
out_path.write_text(content)
content = '\n'.join(lines_out).encode('utf-8')
else:
content = path.read_bytes()
out_path.write_bytes(content)
zip_fn = Path(shutil.make_archive(f'{pkg_dir.name}-v{version}', 'zip', pkg_dir, '.'))
if not dry_run:
@ -111,7 +113,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}')

View file

@ -1,6 +1,6 @@
[project]
name = "kicoil"
version = "0.9.0"
version = "0.11.1"
description = "Planar Inductor Generator"
readme = "README.rst"
license = "Apache-2.0"
@ -48,6 +48,7 @@ kicoil-gui = "kicoil.gui:main"
[dependency-groups]
dev = [
"ipykernel>=7.1.0",
"matplotlib>=3.10.8",
]
gui = ["cairosvg", "pillow"]
gds = ["gdstk"]

View file

@ -90,15 +90,16 @@ def circle_center_to_tangents(center, a, b):
@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.option('--geometry-debug-file', type=click.Path(writable=True), help='Render geometry debug information to a PDF file with the given name')
@click.version_option()
@click.pass_context
def cli(ctx, footprint_name, cell_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, geometry_debug_file, **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, cell_name
nonlocal footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, cell_name, geometry_debug_file
logger = logging.getLogger('kicoil')
if single_layer:
@ -112,7 +113,7 @@ def cli(ctx, footprint_name, cell_name, clipboard, single_layer, arc_tolerance,
if footprint_name is None and outfile:
footprint_name = outfile.stem
footprint = model.render_footprint(footprint_name, arc_tolerance, circle_segments)
footprint = model.render_footprint(footprint_name, arc_tolerance, circle_segments, geometry_debug_file)
except ValueError as e:
#raise click.ClickException(*e.args)

View file

@ -94,6 +94,15 @@ def arc_approximate(points, trace_width, layer, tolerance=0.02, level=0):
yield kicad.make_arc(x2, y2, x0, y0, x1, y1, trace_width, layer)
def polygon_is_clockwise(coords):
# https://en.wikipedia.org/wiki/Curve_orientation
xb, yb, i = min([(x, y, i) for i, (x, y) in enumerate(coords)])
xa, ya = coords[(i-1) % len(coords)]
xc, yc = coords[(i+1) % len(coords)]
det = (xa*yb + xb*yc + xc*ya) - (xa*yc + xb * ya + xc * yb)
return det < 0
class Shape:
pass
@ -119,7 +128,7 @@ class CircleShape(Shape):
return f'{self.outer_diameter:.2f} mm OD, {self.inner_diameter:.2f} mm ID circular'
def compute_spiral(self, a1, a2, fn=64):
def compute_spiral(self, a1, a2, fn=64, debug=False):
r1, r2 = self.outer_radius, self.inner_radius
fn = ceil(fn * abs(a2-a1)/(2*pi))
x0, y0 = cos(a1)*r1, sin(a1)*r1
@ -153,6 +162,8 @@ class CircleShape(Shape):
class OffsetShape(Shape):
def __post_init__(self):
self.sk = skeletonator.Skeletonator(self.polygon)
if self.annular_width > self.sk.min_radius:
raise ValueError(f'Annular width ({self.annular_width:.2f}) is too large. Must be less than {self.sk.min_radius:.2f}')
self.outer_radius = self.sk.radius
self.inner_radius = self.sk.radius - self.annular_width
@ -166,11 +177,11 @@ class OffsetShape(Shape):
return f'polygonal (n={len(self.polygon)} point, r={self.radius:.2f} mm radius)'
def compute_spiral(self, a1, a2, fn=None):
def compute_spiral(self, a1, a2, fn=None, debug=False):
# Skeletonator uses a t coordinate from 0 - 1 per revolution instead of a radian angle.
points = []
angle_refs = []
for point, angle_ref in self.sk.do_spiral(a1/(2*pi), a2/(2*pi), self.outer_radius, self.inner_radius):
for point, angle_ref in self.sk.do_spiral(a1/(2*pi), a2/(2*pi), self.outer_radius, self.inner_radius, debug=debug):
points.append(point)
angle_refs.append(angle_ref)
if a2 < a1:
@ -329,17 +340,22 @@ class SVGShape(OffsetShape):
d = path.attrs['d']
d = d.strip('MmZ ').replace(',', 'L')
coord_pairs = d.split('L')
coords = list(reversed([tuple(map(float, pair.split())) for pair in coord_pairs]))
# Calculate bounding box
min_x = min(x for x, _y in coords)
min_y = max(x for x, _y in coords)
max_x = min(y for _x, y in coords)
max_y = max(y for _x, y in coords)
if max_x < 0 or max_y < 0 or min_x > 0 or min_y > 0:
# (0, 0) is not within the polygon's axis-aligned bounding box, recenter.
ox, oy = skeletonator.polygon_center_of_mass(coords)
warnings.warn(f'Polygon looks not centered, bounds are ({min_x:.2f}, {min_y:.2f}), ({max_x:.2f}, {max_y:.2f}). Aligning (0, 0) with polygon centroid at ({ox:.2f}, {oy:.2f})')
coords = [(x-ox, y-oy) for x, y in coords]
coords = [tuple(map(float, pair.split())) for pair in coord_pairs]
if polygon_is_clockwise(coords):
coords = coords[::-1]
# Calculate bounding box
min_x = min(x for x, _y in coords)
min_y = max(x for x, _y in coords)
max_x = min(y for _x, y in coords)
max_y = max(y for _x, y in coords)
if max_x < 0 or max_y < 0 or min_x > 0 or min_y > 0:
# (0, 0) is not within the polygon's axis-aligned bounding box, recenter.
ox, oy = skeletonator.polygon_center_of_mass(coords)
warnings.warn(f'Polygon looks not centered, bounds are ({min_x:.2f}, {min_y:.2f}), ({max_x:.2f}, {max_y:.2f}). Aligning (0, 0) with polygon centroid at ({ox:.2f}, {oy:.2f})')
coords = [(x-ox, y-oy) for x, y in coords]
self.polygon = coords
super().__post_init__()
@ -427,10 +443,10 @@ class PlanarInductor():
else:
if round(self.trace_width, 2) > round(self.projected_spiral_pitch, 2):
raise click.ClickException(f'Error: Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
raise ValueError(f'Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
clearance_actual = self.projected_spiral_pitch - self.trace_width
if round(clearance_actual, 3) < round(self.clearance, 3):
raise click.ClickException(f'Error: Actual clearance for {self.trace_width:.2f} mm trace is {clearance_actual:.2f} mm, which is lower than the given clearance of {self.clearance:.2f} mm.')
raise ValueError(f'Actual clearance for {self.trace_width:.2f} mm trace is {clearance_actual:.2f} mm, which is lower than the given clearance of {self.clearance:.2f} mm.')
if round(self.via_diameter, 2) < round(self.trace_width, 2):
self.logger.warning(f'Clipping via diameter from {self.via_diameter:.2f} mm to trace width of {self.trace_width:.2f} mm.')
@ -491,7 +507,7 @@ class PlanarInductor():
def default_footprint_name(self):
return f'planar-coil-{self.shape.slug}-n{self.turns}-k{self.twists}'
def render_footprint(self, name=None, arc_tolerance=0.02, circle_segments=64):
def render_footprint(self, name=None, arc_tolerance=0.02, circle_segments=64, geometry_debug_file=None):
if name is None:
name = self.default_footprint_name
@ -519,7 +535,7 @@ class PlanarInductor():
end_angle = fold_angle + self.sweeping_angle
# Handle the spiral arm
points_layer0, arm_length, angle_refs_layer0 = self.shape.compute_spiral(a1=start_angle, a2=fold_angle, fn=circle_segments)
points_layer0, arm_length, angle_refs_layer0 = self.shape.compute_spiral(a1=start_angle, a2=fold_angle, fn=circle_segments, debug=True)
x0, y0 = points_layer0[0]
xn, yn = points_layer0[-1]
if angle_refs_layer0:
@ -534,7 +550,7 @@ class PlanarInductor():
if self.layers > 1:
# 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, _, angle_refs_layer1 = self.shape.compute_spiral(a1=end_angle, a2=fold_angle, fn=circle_segments, debug=True)
points_layer1 = points_layer1[::-1]
if self.approximate_arcs and isinstance(self.shape, CircleShape):
footprint.arcs.extend(arc_approximate(points_layer1, self.trace_width, self.layer_pair[1], arc_tolerance))
@ -611,5 +627,7 @@ class PlanarInductor():
keepout=ZoneKeepout(copperpour_allowed=False),
polygon=ZonePolygon(pts=[XYCoord(x=x, y=y) for x, y in pts])))
if geometry_debug_file:
self.shape.sk.dump_to_pdf(geometry_debug_file)
return footprint

View file

@ -27,7 +27,7 @@ from pathlib import Path
from contextlib import contextmanager
from io import BytesIO
from .geometry import PlanarInductor, divisors, CircleShape
from .geometry import PlanarInductor, divisors, CircleShape, SectorShape, StarShape, SVGShape, RectangleShape, RegularPolygonShape, TrapezoidShape
from .svg import make_transparent_svg
try:
@ -128,6 +128,9 @@ class KiCoilGUI:
self.root.title("KiCoil - Planar Inductor Generator")
self.root.geometry("1000x650")
# Register validation command for non-negative numbers early
self.validate_nonneg_cmd = self.root.register(self.validate_nonnegative)
style = ttk.Style()
style.theme_use('clam')
@ -159,6 +162,44 @@ class KiCoilGUI:
self.notebook.add(geometry_frame, text="Geometry")
self.create_geometry_params(geometry_frame)
# Shape parameter tabs (will be shown/hidden based on selection)
self.shape_param_frames = {}
circle_frame = ttk.Frame(self.notebook, padding="10")
self.shape_param_frames["Circle"] = circle_frame
self.notebook.add(circle_frame, text="Circle Parameters")
self.create_circle_params(circle_frame)
rectangle_frame = ttk.Frame(self.notebook, padding="10")
self.shape_param_frames["Rectangle"] = rectangle_frame
self.notebook.add(rectangle_frame, text="Rectangle Parameters")
self.create_rectangle_params(rectangle_frame)
trapezoid_frame = ttk.Frame(self.notebook, padding="10")
self.shape_param_frames["Trapezoid"] = trapezoid_frame
self.notebook.add(trapezoid_frame, text="Trapezoid Parameters")
self.create_trapezoid_params(trapezoid_frame)
sector_frame = ttk.Frame(self.notebook, padding="10")
self.shape_param_frames["Sector"] = sector_frame
self.notebook.add(sector_frame, text="Sector Parameters")
self.create_sector_params(sector_frame)
star_frame = ttk.Frame(self.notebook, padding="10")
self.shape_param_frames["Star"] = star_frame
self.notebook.add(star_frame, text="Star Parameters")
self.create_star_params(star_frame)
polygon_frame = ttk.Frame(self.notebook, padding="10")
self.shape_param_frames["Regular Polygon"] = polygon_frame
self.notebook.add(polygon_frame, text="Polygon Parameters")
self.create_polygon_params(polygon_frame)
svg_frame = ttk.Frame(self.notebook, padding="10")
self.shape_param_frames["SVG"] = svg_frame
self.notebook.add(svg_frame, text="SVG Parameters")
self.create_svg_params(svg_frame)
traces_frame = ttk.Frame(self.notebook, padding="10")
self.notebook.add(traces_frame, text="Traces")
self.create_trace_params(traces_frame)
@ -214,13 +255,26 @@ class KiCoilGUI:
self.preview_label.pack(fill=tk.BOTH, expand=True)
self.preview_frame.grid(row=0, column=1, sticky=(tk.N, tk.S, tk.E, tk.W), padx=(10, 10), pady=10)
self.current_model = None
self._validation_after_id = None
self.setup_logging()
self.setup_traces()
self.update_shape_tab_visibility() # Initialize tab visibility
self.root.after(100, self.validate_parameters)
def validate_nonnegative(self, value_if_allowed):
"""Validation callback for spinboxes to prevent negative values"""
if value_if_allowed == "" or value_if_allowed == "-":
# Allow empty string (user is typing) but not standalone minus
return value_if_allowed == ""
try:
float_val = float(value_if_allowed)
return float_val >= 0
except ValueError:
return False
def _on_preview_resize(self, event):
# Debounce resize events - only update after resize is complete
if hasattr(self, '_resize_after_id') and self._resize_after_id is not None:
@ -280,11 +334,23 @@ class KiCoilGUI:
def create_geometry_params(self, parent):
row = 0
# Shape Type
ttk.Label(parent, text="Shape Type:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.shape_type_var = tk.StringVar(value="Circle")
shape_combo = ttk.Combobox(parent, textvariable=self.shape_type_var,
values=["Circle", "Rectangle", "Trapezoid", "Sector", "Star", "Regular Polygon", "SVG"],
state='readonly', width=23)
shape_combo.grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Coil outline shape",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
# Turns
ttk.Label(parent, text="Number of Turns:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.turns_var = tk.IntVar(value=7)
ttk.Spinbox(parent, from_=1, to=100, textvariable=self.turns_var,
width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Number of spiral turns",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
@ -293,29 +359,12 @@ class KiCoilGUI:
ttk.Label(parent, text="Twists per Revolution:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.twists_var = tk.IntVar(value=4)
ttk.Spinbox(parent, from_=0, to=50, textvariable=self.twists_var,
width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Must be co-prime to turns",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
# Outer Diameter
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.outer_dia_var = tk.DoubleVar(value=50.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.outer_dia_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Outside diameter of coil",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
# Inner Diameter
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.inner_dia_var = tk.DoubleVar(value=25.0)
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
textvariable=self.inner_dia_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Inside diameter of coil",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
# Layer Mode
ttk.Label(parent, text="Layer Mode:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.layer_mode_var = tk.IntVar(value=2)
@ -338,6 +387,221 @@ class KiCoilGUI:
value="clockwise").pack(side=tk.LEFT)
row += 1
def create_circle_params(self, parent):
row = 0
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.circle_outer_dia_var = tk.DoubleVar(value=50.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.circle_outer_dia_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Outside diameter of coil",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.circle_inner_dia_var = tk.DoubleVar(value=25.0)
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
textvariable=self.circle_inner_dia_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Inside diameter of coil",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def create_rectangle_params(self, parent):
row = 0
ttk.Label(parent, text="Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.rect_width_var = tk.DoubleVar(value=50.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.rect_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Height (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.rect_height_var = tk.DoubleVar(value=40.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.rect_height_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.rect_annular_width_var = tk.DoubleVar(value=10.0)
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
textvariable=self.rect_annular_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Width of trace area",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def create_trapezoid_params(self, parent):
row = 0
ttk.Label(parent, text="Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.trap_width_var = tk.DoubleVar(value=50.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.trap_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Height (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.trap_height_var = tk.DoubleVar(value=40.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.trap_height_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Offset (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.trap_offset_var = tk.DoubleVar(value=10.0)
ttk.Spinbox(parent, from_=0, to=100, increment=0.5,
textvariable=self.trap_offset_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Corner offset at shorter edge",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.trap_annular_width_var = tk.DoubleVar(value=10.0)
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
textvariable=self.trap_annular_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Width of trace area",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def create_sector_params(self, parent):
row = 0
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.sector_outer_dia_var = tk.DoubleVar(value=50.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.sector_outer_dia_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.sector_inner_dia_var = tk.DoubleVar(value=25.0)
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
textvariable=self.sector_inner_dia_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Angle (degrees):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.sector_angle_var = tk.DoubleVar(value=45.0)
ttk.Spinbox(parent, from_=1, to=360, increment=1.0,
textvariable=self.sector_angle_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Sector angle",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.sector_annular_width_var = tk.DoubleVar(value=5.0)
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
textvariable=self.sector_annular_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Width of trace area",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def create_star_params(self, parent):
row = 0
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.star_outer_dia_var = tk.DoubleVar(value=50.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.star_outer_dia_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.star_inner_dia_var = tk.DoubleVar(value=25.0)
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
textvariable=self.star_inner_dia_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Points:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.star_points_var = tk.IntVar(value=5)
ttk.Spinbox(parent, from_=3, to=20, textvariable=self.star_points_var,
width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Number of star points",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.star_annular_width_var = tk.DoubleVar(value=5.0)
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
textvariable=self.star_annular_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Width of trace area",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def create_polygon_params(self, parent):
row = 0
ttk.Label(parent, text="Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.poly_diameter_var = tk.DoubleVar(value=50.0)
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
textvariable=self.poly_diameter_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
ttk.Label(parent, text="Corners:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.poly_corners_var = tk.IntVar(value=8)
ttk.Spinbox(parent, from_=3, to=20, textvariable=self.poly_corners_var,
width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Number of polygon corners",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.poly_annular_width_var = tk.DoubleVar(value=10.0)
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
textvariable=self.poly_annular_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Width of trace area",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def create_svg_params(self, parent):
row = 0
ttk.Label(parent, text="SVG File:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.svg_filename_var = tk.StringVar(value="")
svg_entry = ttk.Entry(parent, textvariable=self.svg_filename_var, width=30)
svg_entry.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5)
ttk.Button(parent, text="Browse...", command=self.browse_svg_file, width=10).grid(row=row, column=2, sticky=tk.W, padx=(5, 0), pady=5)
row += 1
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.svg_annular_width_var = tk.DoubleVar(value=5.0)
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
textvariable=self.svg_annular_width_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Width of trace area",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def browse_svg_file(self):
filename = filedialog.askopenfilename(
title="Select SVG File",
filetypes=[("SVG files", "*.svg"), ("All files", "*.*")]
)
if filename:
self.svg_filename_var.set(filename)
def update_shape_tab_visibility(self, *args):
"""Show only the shape parameter tab for the currently selected shape"""
selected_shape = self.shape_type_var.get()
# Hide all shape parameter tabs
for shape_name, frame in self.shape_param_frames.items():
tab_id = self.notebook.index(frame)
self.notebook.tab(tab_id, state='hidden')
# Show only the selected shape's tab
if selected_shape in self.shape_param_frames:
frame = self.shape_param_frames[selected_shape]
tab_id = self.notebook.index(frame)
self.notebook.tab(tab_id, state='normal')
def create_trace_params(self, parent):
row = 0
@ -357,7 +621,8 @@ class KiCoilGUI:
ttk.Label(parent, text="Copper Thickness (µm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.copper_thickness_var = tk.DoubleVar(value=35.0) # 35µm = 0.035mm = 1 Oz
ttk.Spinbox(parent, from_=1, to=1000, increment=1, format="%.1f",
textvariable=self.copper_thickness_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
textvariable=self.copper_thickness_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="35µm = 1 Oz copper",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
@ -370,7 +635,8 @@ class KiCoilGUI:
ttk.Label(parent, text="Via Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.via_diameter_var = tk.DoubleVar(value=0.6)
ttk.Spinbox(parent, from_=0.1, to=5.0, increment=0.1, format="%.2f",
textvariable=self.via_diameter_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
textvariable=self.via_diameter_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
# Via Drill
@ -429,7 +695,8 @@ class KiCoilGUI:
ttk.Label(parent, text="Circle Segments:").grid(row=row, column=0, sticky=tk.W, pady=5)
self.circle_segments_var = tk.IntVar(value=64)
ttk.Spinbox(parent, from_=8, to=360, textvariable=self.circle_segments_var,
width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Points per 360° for arc interpolation",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
@ -438,7 +705,8 @@ class KiCoilGUI:
ttk.Label(parent, text="Arc Tolerance (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.arc_tolerance_var = tk.DoubleVar(value=0.02)
ttk.Spinbox(parent, from_=0.001, to=1.0, increment=0.001, format="%.3f",
textvariable=self.arc_tolerance_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
textvariable=self.arc_tolerance_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
row += 1
# Keepout Zone
@ -452,19 +720,65 @@ class KiCoilGUI:
ttk.Label(parent, text="Keepout Margin (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
self.keepout_margin_var = tk.DoubleVar(value=5.0)
ttk.Spinbox(parent, from_=0, to=50, increment=0.5,
textvariable=self.keepout_margin_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
textvariable=self.keepout_margin_var, width=15,
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
ttk.Label(parent, text="Margin around coil",
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
row += 1
def get_parameters(self):
shape_params = {
'outer_diameter' : self.outer_dia_var.get(),
'inner_diameter' : self.inner_dia_var.get(),
}
# Create the appropriate shape based on selection
shape_type = self.shape_type_var.get()
if shape_type == "Circle":
shape = CircleShape(
outer_diameter=self.circle_outer_dia_var.get(),
inner_diameter=self.circle_inner_dia_var.get()
)
elif shape_type == "Rectangle":
shape = RectangleShape(
width=self.rect_width_var.get(),
height=self.rect_height_var.get(),
annular_width=self.rect_annular_width_var.get()
)
elif shape_type == "Trapezoid":
shape = TrapezoidShape(
width=self.trap_width_var.get(),
height=self.trap_height_var.get(),
offset=self.trap_offset_var.get(),
annular_width=self.trap_annular_width_var.get()
)
elif shape_type == "Sector":
import math
shape = SectorShape(
inner_diameter=self.sector_inner_dia_var.get(),
outer_diameter=self.sector_outer_dia_var.get(),
angle=math.radians(self.sector_angle_var.get()),
annular_width=self.sector_annular_width_var.get()
)
elif shape_type == "Star":
shape = StarShape(
inner_diameter=self.star_inner_dia_var.get(),
outer_diameter=self.star_outer_dia_var.get(),
points=self.star_points_var.get(),
annular_width=self.star_annular_width_var.get()
)
elif shape_type == "Regular Polygon":
shape = RegularPolygonShape(
diameter=self.poly_diameter_var.get(),
corners=self.poly_corners_var.get(),
annular_width=self.poly_annular_width_var.get()
)
elif shape_type == "SVG":
shape = SVGShape(
filename=self.svg_filename_var.get(),
annular_width=self.svg_annular_width_var.get()
)
else:
raise ValueError(f"Unknown shape type: {shape_type}")
params = {
'shape' : CircleShape(**shape_params),
'shape' : shape,
'turns' : self.turns_var.get(),
'layers' : self.layer_mode_var.get(),
'twists' : self.twists_var.get(),
@ -517,10 +831,12 @@ class KiCoilGUI:
warnings.showwarning = old_showwarning
def setup_traces(self):
# Shape type needs special handling for tab visibility
self.shape_type_var.trace_add('write', self.update_shape_tab_visibility)
self.shape_type_var.trace_add('write', self._on_parameter_change)
for var in [
self.turns_var,
self.outer_dia_var,
self.inner_dia_var,
self.layer_mode_var,
self.direction_var,
self.twists_var,
@ -531,13 +847,44 @@ class KiCoilGUI:
self.stagger_inner_var,
self.stagger_outer_var,
self.top_layer_var,
self.bottom_layer_var]:
self.bottom_layer_var,
# Circle shape params
self.circle_outer_dia_var,
self.circle_inner_dia_var,
# Rectangle shape params
self.rect_width_var,
self.rect_height_var,
self.rect_annular_width_var,
# Trapezoid shape params
self.trap_width_var,
self.trap_height_var,
self.trap_offset_var,
self.trap_annular_width_var,
# Sector shape params
self.sector_outer_dia_var,
self.sector_inner_dia_var,
self.sector_angle_var,
self.sector_annular_width_var,
# Star shape params
self.star_outer_dia_var,
self.star_inner_dia_var,
self.star_points_var,
self.star_annular_width_var,
# Polygon shape params
self.poly_diameter_var,
self.poly_corners_var,
self.poly_annular_width_var,
# SVG shape params
self.svg_annular_width_var]:
var.trace_add('write', self._on_parameter_change)
for entry in [self.trace_width_entry, self.clearance_entry,
self.via_drill_entry, self.via_offset_entry,
self.footprint_name_entry]:
entry.bind('<KeyRelease>', lambda e: self._on_parameter_change())
# SVG filename entry needs special handling
self.svg_filename_var.trace_add('write', self._on_parameter_change)
def _on_parameter_change(self, *args):
# Schedule validation to avoid too many rapid calls
@ -561,15 +908,21 @@ class KiCoilGUI:
return True
except ValueError as e:
self.output_text['state'] = 'normal'
self.output_text.insert(tk.END, f"ERROR: {e}\n", 'error')
self.output_text.see(tk.END)
tb = traceback.format_exc()
print(tb, file=sys.stderr)
self.current_model = None
self.update_placeholders()
self.update_preview()
return False
except Exception as e:
tb = traceback.format_exc()
self.output_text['state'] = 'normal'
self.output_text.insert(tk.END, f"Unexpected error:\n{tb}\n", 'error')
self.output_text.see(tk.END)
@ -577,7 +930,8 @@ class KiCoilGUI:
self.current_model = None
self.update_placeholders()
return True
self.update_preview()
return False
finally:
self.output_text['state'] = 'disabled'
@ -610,6 +964,13 @@ class KiCoilGUI:
if not HAS_PREVIEW:
return
if self.current_model is None:
# Clear preview when model is invalid
self.preview_canvas.delete("all")
self.preview_raw_image = None
self.preview_image = None
return
arc_tolerance = self.arc_tolerance_var.get()
circle_segments = self.circle_segments_var.get()

View file

@ -11,8 +11,6 @@ import sys
import hashlib
import platformdirs
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
import wasmtime
@ -111,7 +109,7 @@ class WasmApp:
self.app.exports(store)["_start"](store)
except wasmtime.ExitTrap as trap:
if trap.code != 0:
raise
raise RuntimeError('Error computing straight skeleton.')
return 0, stdout_f.read()
@ -189,15 +187,13 @@ class Skeletonator:
for x, y in poly:
p = (round(x, 6), round(y, 6))
self.node_map[(x, y)] = coord_map[p]
self.dump_to_pdf('/tmp/test.pdf')
self.debug_arms = []
def iter_arcs(self, p):
i = 0
start = self.node_map[p]
#print('start', start, start in self.arc_map, start in self.divergent)
while start in self.arc_map and not start in self.divergent:
end = self.arc_map[start]
#print('end', i, end)
i += 1
yield start, end
start = end
@ -246,13 +242,15 @@ class Skeletonator:
points.append(pt)
return arcs, points
def do_spiral(self, t1, t2, r1=None, r2=None):
def do_spiral(self, t1, t2, r1=None, r2=None, debug=False):
if r1 is None:
r1 = self.radius
if r2 is None:
r2 = self.min_radius
direction = True
if t2 < t1:
direction = False
t1, t2 = t2, t1
r1, r2 = r2, r1
@ -260,15 +258,17 @@ class Skeletonator:
t = max(t1, min(t2, t)) # Clip to start/end of spiral
f = (t - t1) / (t2 - t1)
return r1 + (r2 - r1) * f
for t_start in range(math.floor(t1), math.ceil(t2)):
debug_arm = []
for t_start in range(math.ceil(t2-t1)):
t_start += t1
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
_ic_arcs, inner_circumference = self.map_circumference(r_ref)
angle = t_start
angle = math.floor(t_start)
circumference_angles = []
inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference))
point_angles = []
@ -276,52 +276,86 @@ class Skeletonator:
edge_angle = math.dist(p1, p2) / inner_circumference_sum
point_angles.append(angle)
angle += edge_angle
point_angles.append(t_end)
point_angles += [a+1 for a in point_angles]
point_angles += [point_angles[0] + 2]
for (p1, p2), (tp1, tp2) in zip(self.poly_edges, itertools.pairwise(point_angles)):
i = 0
for (p1, p2), (tp1, tp2) in zip(self.poly_edges * 3, itertools.pairwise(point_angles)):
i += 1
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 tp2 < t_start and not math.isclose(tp2, t_start):
continue
if tp1 > t_end and not math.isclose(tp1, t_end):
continue
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
p_out = interpolate(p1_proj, p2_proj_r1, t1, tp1, tp2)
debug_arm.append(p_out)
yield p_out, 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
p_out = interpolate(p1_proj_r2, p2_proj, t2, tp1, tp2)
debug_arm.append(p_out)
yield p_out, r_ref
elif approx_in_range(tp2, t1, t2):
debug_arm.append(p2_proj)
yield p2_proj, r_ref
if debug:
self.debug_arms.append((debug_arm, direction, t1, t2))
def dump_to_pdf(self, filename):
with PdfPages(filename) as pdf:
fig, ax = plt.subplots(figsize=(10, 10))
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 10))
# polygon outline
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')
ax.plot(poly_x, poly_y, 'bo', markersize=4)
# polygon outline
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, '-', color='black', linewidth=.5, label='Polygon')
# skeleton edges
for node1, node2 in self.skeleton_edges:
ax.plot([node1.x, node2.x], [node1.y, node2.y], 'r-', linewidth=1, alpha=0.7)
# skeleton edges
for node1, node2 in self.skeleton_edges:
ax.plot([node1.x, node2.x], [node1.y, node2.y], '-', color='gray', linewidth=.5, alpha=0.7)
# skeleton nodes
for n in self.skeleton_nodes:
if n in self.divergent:
ax.plot(n.x, n.y, 'go', markersize=6)
elif n in self.arc_map:
ax.plot(n.x, n.y, 'ro', markersize=3, alpha=0.5)
else:
ax.plot(n.x, n.y, 'o', color='magenta', markersize=6)
# skeleton nodes
for n in self.skeleton_nodes:
if n in self.divergent:
ax.plot(n.x, n.y, 'o', markerfacecolor='none', markeredgecolor='green', markersize=4)
elif n in self.arc_map:
ax.plot(n.x, n.y, 'o', color='black', markersize=3, alpha=0.5)
else:
ax.plot(n.x, n.y, 'o', markerfacecolor='none', markeredgecolor='magenta', markersize=4)
ax.set_aspect('equal', adjustable='box')
ax.grid(True, alpha=0.3)
ax.legend()
ax.set_title(f'Polygon Skeleton (radius: {self.radius:.3f}, min_radius: {self.min_radius:.3f})')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.invert_yaxis()
pdf.savefig(fig, bbox_inches='tight')
plt.close(fig)
count = {True: 0, False: 0}
for arm, direction, t1, t2 in self.debug_arms:
xs = [x for x, y in arm]
ys = [y for x, y in arm]
ax.plot(xs, ys, linewidth=.2, color='red' if direction else 'blue')
align = 'left' if direction else 'right'
ax.text(xs[-1], ys[-1], f'{count[direction]}', size=3, horizontalalignment=align)
ax.text(xs[0], ys[0], f'{count[direction]}', size=3, horizontalalignment=align, color='gray')
count[direction] += 1
xs, ys = [], []
for i in range(100):
r = self.radius - (i/99) * self.min_radius
arc, (px, py) = self.project_arc(self.poly[0], r)
xs.append(px)
ys.append(py)
ax.plot(xs, ys, '--', linewidth=.5, color='black')
ax.set_aspect('equal', adjustable='box')
ax.grid(True, alpha=0.3)
ax.legend()
ax.set_title(f'Polygon Skeleton (radius: {self.radius:.3f}, min_radius: {self.min_radius:.3f})')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.invert_yaxis()
fig.savefig(filename, bbox_inches='tight', dpi=600)
plt.close(fig)

8
uv.lock generated
View file

@ -569,7 +569,7 @@ wheels = [
[[package]]
name = "kicoil"
version = "0.9.0"
version = "0.11.1"
source = { editable = "." }
dependencies = [
{ name = "beautifulsoup4" },
@ -585,6 +585,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "ipykernel" },
{ name = "matplotlib" },
]
gds = [
{ name = "gdstk" },
@ -607,7 +608,10 @@ requires-dist = [
]
[package.metadata.requires-dev]
dev = [{ name = "ipykernel", specifier = ">=7.1.0" }]
dev = [
{ name = "ipykernel", specifier = ">=7.1.0" },
{ name = "matplotlib", specifier = ">=3.10.8" },
]
gds = [{ name = "gdstk" }]
gui = [
{ name = "cairosvg" },