Compare commits
19 commits
experiment
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe1f457dd | ||
|
|
800d705e02 | ||
|
|
eed50a097e | ||
|
|
c712473064 | ||
|
|
67eb82fab5 | ||
|
|
c04ae73986 | ||
|
|
5893ef234e | ||
|
|
7cac7b1041 | ||
|
|
df2e0c7bcf | ||
|
|
6666e665e2 | ||
|
|
1140c5bca3 | ||
|
|
2b64ee5081 | ||
|
|
e54517544a | ||
|
|
57e8a52e1f | ||
|
|
b25b32e98f | ||
|
|
96d06a8dc3 | ||
|
|
b0b942431e | ||
|
|
82d107fc82 | ||
|
|
4295b2d9f6 |
11 changed files with 550 additions and 110 deletions
|
|
@ -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
BIN
de.jaseg.kicoil-v0.11.0.zip
Normal file
Binary file not shown.
BIN
de.jaseg.kicoil-v0.11.1.zip
Normal file
BIN
de.jaseg.kicoil-v0.11.1.zip
Normal file
Binary file not shown.
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
package.py
10
package.py
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
8
uv.lock
generated
|
|
@ -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" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue