Compare commits
6 commits
main
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82eccbad1d | ||
|
|
87033c116f | ||
|
|
556707dc35 | ||
|
|
3e6c7d6f57 | ||
|
|
5bdf4d3274 | ||
|
|
7bfaabc839 |
11 changed files with 181 additions and 578 deletions
|
|
@ -52,8 +52,7 @@ int main()
|
|||
}
|
||||
|
||||
if (!poly.is_counterclockwise_oriented()) {
|
||||
std::cerr << "Error: Polygon must be counter-clockwise" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
poly.reverse_orientation();
|
||||
}
|
||||
|
||||
SsPtr ss = CGAL::create_interior_straight_skeleton_2(poly.vertices_begin(), poly.vertices_end(), K());
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"$schema": "https://go.kicad.org/pcm/schemas/v1",
|
||||
"name": "KiCoil",
|
||||
"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.",
|
||||
"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.",
|
||||
"identifier": "de.jaseg.kicoil",
|
||||
"type": "plugin",
|
||||
"author": {
|
||||
|
|
@ -26,25 +26,7 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -79,18 +79,16 @@ 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).encode('utf-8')
|
||||
else:
|
||||
content = path.read_bytes()
|
||||
out_path.write_bytes(content)
|
||||
content = '\n'.join(lines_out)
|
||||
out_path.write_text(content)
|
||||
|
||||
zip_fn = Path(shutil.make_archive(f'{pkg_dir.name}-v{version}', 'zip', pkg_dir, '.'))
|
||||
if not dry_run:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "kicoil"
|
||||
version = "0.11.1"
|
||||
version = "0.10.0"
|
||||
description = "Planar Inductor Generator"
|
||||
readme = "README.rst"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -48,7 +48,6 @@ kicoil-gui = "kicoil.gui:main"
|
|||
[dependency-groups]
|
||||
dev = [
|
||||
"ipykernel>=7.1.0",
|
||||
"matplotlib>=3.10.8",
|
||||
]
|
||||
gui = ["cairosvg", "pillow"]
|
||||
gds = ["gdstk"]
|
||||
|
|
|
|||
|
|
@ -90,16 +90,15 @@ 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, geometry_debug_file, **kwargs):
|
||||
def cli(ctx, footprint_name, cell_name, clipboard, single_layer, arc_tolerance, circle_segments, format, **kwargs):
|
||||
ctx.ensure_object(dict)
|
||||
logger = logging.getLogger('kicoil')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def write(shape, outfile):
|
||||
nonlocal footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, cell_name, geometry_debug_file
|
||||
nonlocal footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, cell_name
|
||||
logger = logging.getLogger('kicoil')
|
||||
|
||||
if single_layer:
|
||||
|
|
@ -113,7 +112,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, geometry_debug_file)
|
||||
footprint = model.render_footprint(footprint_name, arc_tolerance, circle_segments)
|
||||
|
||||
except ValueError as e:
|
||||
#raise click.ClickException(*e.args)
|
||||
|
|
|
|||
|
|
@ -94,15 +94,6 @@ 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
|
||||
|
||||
|
|
@ -128,7 +119,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, debug=False):
|
||||
def compute_spiral(self, a1, a2, fn=64):
|
||||
r1, r2 = self.outer_radius, self.inner_radius
|
||||
fn = ceil(fn * abs(a2-a1)/(2*pi))
|
||||
x0, y0 = cos(a1)*r1, sin(a1)*r1
|
||||
|
|
@ -151,6 +142,8 @@ class CircleShape(Shape):
|
|||
def project_point(self, r, a, r_ref=None):
|
||||
return cos(a) * r, sin(a) * r
|
||||
|
||||
def map_angle(self, a):
|
||||
return a
|
||||
|
||||
def offset_exterior(self, margin):
|
||||
r = self.outer_radius + margin
|
||||
|
|
@ -162,8 +155,6 @@ 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
|
||||
|
||||
|
|
@ -177,11 +168,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, debug=False):
|
||||
def compute_spiral(self, a1, a2, fn=None):
|
||||
# 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, debug=debug):
|
||||
for point, angle_ref in self.sk.do_spiral(a1/(2*pi), a2/(2*pi), self.outer_radius, self.inner_radius):
|
||||
points.append(point)
|
||||
angle_refs.append(angle_ref)
|
||||
if a2 < a1:
|
||||
|
|
@ -191,6 +182,12 @@ class OffsetShape(Shape):
|
|||
return points, arm_length, angle_refs
|
||||
|
||||
|
||||
def map_angle(self, a):
|
||||
a_new = self.sk.map_angle(a / (2*pi), self.outer_radius, self.inner_radius)
|
||||
print(f'NEW MAPPED {a:.3f} to {a_new:.3f}')
|
||||
return a_new * 2 * pi
|
||||
|
||||
|
||||
def project_point(self, r, a, r_ref=None):
|
||||
# Skeletonator uses a t coordinate from 0 - 1 per revolution instead of a radian angle.
|
||||
return self.sk.project_point(a/(2*pi) % 1, r, r_ref=r_ref)
|
||||
|
|
@ -340,22 +337,17 @@ class SVGShape(OffsetShape):
|
|||
d = path.attrs['d']
|
||||
d = d.strip('MmZ ').replace(',', 'L')
|
||||
coord_pairs = d.split('L')
|
||||
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]
|
||||
|
||||
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]
|
||||
self.polygon = coords
|
||||
super().__post_init__()
|
||||
|
||||
|
|
@ -443,10 +435,10 @@ class PlanarInductor():
|
|||
|
||||
else:
|
||||
if round(self.trace_width, 2) > round(self.projected_spiral_pitch, 2):
|
||||
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.')
|
||||
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.')
|
||||
clearance_actual = self.projected_spiral_pitch - self.trace_width
|
||||
if round(clearance_actual, 3) < round(self.clearance, 3):
|
||||
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.')
|
||||
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.')
|
||||
|
||||
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.')
|
||||
|
|
@ -507,7 +499,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, geometry_debug_file=None):
|
||||
def render_footprint(self, name=None, arc_tolerance=0.02, circle_segments=64):
|
||||
if name is None:
|
||||
name = self.default_footprint_name
|
||||
|
||||
|
|
@ -534,8 +526,9 @@ class PlanarInductor():
|
|||
fold_angle = start_angle + self.sweeping_angle
|
||||
end_angle = fold_angle + self.sweeping_angle
|
||||
|
||||
print(f'### TWIST {i} INWARD ###')
|
||||
# Handle the spiral arm
|
||||
points_layer0, arm_length, angle_refs_layer0 = self.shape.compute_spiral(a1=start_angle, a2=fold_angle, fn=circle_segments, debug=True)
|
||||
points_layer0, arm_length, angle_refs_layer0 = self.shape.compute_spiral(a1=start_angle, a2=fold_angle, fn=circle_segments)
|
||||
x0, y0 = points_layer0[0]
|
||||
xn, yn = points_layer0[-1]
|
||||
if angle_refs_layer0:
|
||||
|
|
@ -549,8 +542,9 @@ class PlanarInductor():
|
|||
footprint.lines.extend(kicad.make_line(*p1, *p2, self.trace_width, self.layer_pair[0]) for p1, p2 in zip(points_layer0, points_layer0[1:]))
|
||||
|
||||
if self.layers > 1:
|
||||
print(f'### TWIST {i} OUTWARD ###')
|
||||
# Handle the returning arm on the bottom layer
|
||||
points_layer1, _, angle_refs_layer1 = self.shape.compute_spiral(a1=end_angle, a2=fold_angle, fn=circle_segments, debug=True)
|
||||
points_layer1, _, angle_refs_layer1 = self.shape.compute_spiral(a1=end_angle, a2=fold_angle, fn=circle_segments)
|
||||
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))
|
||||
|
|
@ -584,8 +578,8 @@ class PlanarInductor():
|
|||
|
||||
xv, yv = self.shape.project_point(r, fold_angle, r_ref=refs_layer0[-1])
|
||||
|
||||
footprint.lines.append(kicad.make_line(*points_layer0[-1], xv, yv, self.trace_width, self.layer_pair[0]))
|
||||
footprint.lines.append(kicad.make_line(xv, yv, *points_layer1[0], self.trace_width, self.layer_pair[1]))
|
||||
#footprint.lines.append(kicad.make_line(*points_layer0[-1], xv, yv, self.trace_width, self.layer_pair[0]))
|
||||
#footprint.lines.append(kicad.make_line(xv, yv, *points_layer1[0], self.trace_width, self.layer_pair[1]))
|
||||
|
||||
footprint.pads.append(kicad.make_via(xv, yv,
|
||||
self.via_diameter, self.via_drill, self.clearance,
|
||||
|
|
@ -602,7 +596,7 @@ class PlanarInductor():
|
|||
points_layer0, refs_layer0 = arms_layers[0][i]
|
||||
points_layer1, refs_layer1 = arms_layers[1][(i - self.turns) % self.twists]
|
||||
|
||||
xv, yv = self.shape.project_point(r, start_angle, r_ref=refs_layer0[0])
|
||||
xv, yv = self.shape.project_point(r, self.shape.map_angle(start_angle), r_ref=refs_layer0[0])
|
||||
|
||||
footprint.lines.append(kicad.make_line(*points_layer0[0], xv, yv, self.trace_width, self.layer_pair[0]))
|
||||
footprint.lines.append(kicad.make_line(*points_layer1[-1], xv, yv, self.trace_width, self.layer_pair[1]))
|
||||
|
|
@ -627,7 +621,5 @@ 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, SectorShape, StarShape, SVGShape, RectangleShape, RegularPolygonShape, TrapezoidShape
|
||||
from .geometry import PlanarInductor, divisors, CircleShape
|
||||
from .svg import make_transparent_svg
|
||||
|
||||
try:
|
||||
|
|
@ -128,9 +128,6 @@ 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')
|
||||
|
||||
|
|
@ -162,44 +159,6 @@ 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)
|
||||
|
|
@ -255,26 +214,13 @@ 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:
|
||||
|
|
@ -334,23 +280,11 @@ 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,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
width=15).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
|
||||
|
|
@ -359,12 +293,29 @@ 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,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
width=15).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)
|
||||
|
|
@ -387,221 +338,6 @@ 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
|
||||
|
||||
|
|
@ -621,8 +357,7 @@ 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,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.copper_thickness_var, width=15).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
|
||||
|
|
@ -635,8 +370,7 @@ 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,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.via_diameter_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Via Drill
|
||||
|
|
@ -695,8 +429,7 @@ 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,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
width=15).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
|
||||
|
|
@ -705,8 +438,7 @@ 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,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.arc_tolerance_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Keepout Zone
|
||||
|
|
@ -720,65 +452,19 @@ 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,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.keepout_margin_var, width=15).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):
|
||||
# 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}")
|
||||
shape_params = {
|
||||
'outer_diameter' : self.outer_dia_var.get(),
|
||||
'inner_diameter' : self.inner_dia_var.get(),
|
||||
}
|
||||
|
||||
params = {
|
||||
'shape' : shape,
|
||||
'shape' : CircleShape(**shape_params),
|
||||
'turns' : self.turns_var.get(),
|
||||
'layers' : self.layer_mode_var.get(),
|
||||
'twists' : self.twists_var.get(),
|
||||
|
|
@ -831,12 +517,10 @@ 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,
|
||||
|
|
@ -847,44 +531,13 @@ class KiCoilGUI:
|
|||
self.stagger_inner_var,
|
||||
self.stagger_outer_var,
|
||||
self.top_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]:
|
||||
self.bottom_layer_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
|
||||
|
|
@ -908,21 +561,15 @@ 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)
|
||||
|
||||
|
|
@ -930,8 +577,7 @@ class KiCoilGUI:
|
|||
|
||||
self.current_model = None
|
||||
self.update_placeholders()
|
||||
self.update_preview()
|
||||
return False
|
||||
return True
|
||||
|
||||
finally:
|
||||
self.output_text['state'] = 'disabled'
|
||||
|
|
@ -964,13 +610,6 @@ 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,6 +11,8 @@ import sys
|
|||
import hashlib
|
||||
|
||||
import platformdirs
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_pdf import PdfPages
|
||||
import wasmtime
|
||||
|
||||
|
||||
|
|
@ -29,6 +31,14 @@ def interpolate(p1, p2, t, t_start=0, t_end=1):
|
|||
return (x1 + t*dx, y1 + t*dy)
|
||||
|
||||
|
||||
def interpolate_1d(a, b, t, t_start=0, t_end=1):
|
||||
if math.isclose(t_start, t_end):
|
||||
return a
|
||||
t_range = t_end - t_start
|
||||
t = (t - t_start) / t_range
|
||||
return a + (b-a) * t
|
||||
|
||||
|
||||
def approx_in_range(value, lower, upper):
|
||||
""" Approximate range check """
|
||||
if math.isclose(value, lower) or math.isclose(value, upper):
|
||||
|
|
@ -109,7 +119,7 @@ class WasmApp:
|
|||
self.app.exports(store)["_start"](store)
|
||||
except wasmtime.ExitTrap as trap:
|
||||
if trap.code != 0:
|
||||
raise RuntimeError('Error computing straight skeleton.')
|
||||
raise
|
||||
return 0, stdout_f.read()
|
||||
|
||||
|
||||
|
|
@ -187,13 +197,15 @@ class Skeletonator:
|
|||
for x, y in poly:
|
||||
p = (round(x, 6), round(y, 6))
|
||||
self.node_map[(x, y)] = coord_map[p]
|
||||
self.debug_arms = []
|
||||
self.dump_to_pdf('/tmp/test.pdf')
|
||||
|
||||
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
|
||||
|
|
@ -241,121 +253,108 @@ class Skeletonator:
|
|||
arcs.append(arc)
|
||||
points.append(pt)
|
||||
return arcs, points
|
||||
|
||||
def do_spiral(self, t1, t2, r1=None, r2=None, debug=False):
|
||||
|
||||
def map_angle(self, t, r1, r2):
|
||||
r_ref = min(r1, r2)
|
||||
_ic_arcs, inner_circumference = self.map_circumference(r_ref)
|
||||
inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference))
|
||||
|
||||
angle = 0
|
||||
point_angles = [0]
|
||||
for p1, p2 in edge_cycle(inner_circumference):
|
||||
edge_angle = math.dist(p1, p2) / inner_circumference_sum
|
||||
angle += edge_angle
|
||||
point_angles.append(angle)
|
||||
|
||||
_oc_arcs, outer_circumference = self.map_circumference(max(r1, r2))
|
||||
outer_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(outer_circumference))
|
||||
|
||||
angle = 0
|
||||
point_angles_outer = [0]
|
||||
for p1, p2 in edge_cycle(outer_circumference):
|
||||
edge_angle = math.dist(p1, p2) / outer_circumference_sum
|
||||
angle += edge_angle
|
||||
point_angles_outer.append(angle)
|
||||
|
||||
t_map_int = math.floor(t)
|
||||
t %= 1.0
|
||||
|
||||
for ia1, ia2, oa1, oa2 in zip(point_angles, point_angles[1:] + [1], point_angles_outer, point_angles_outer[1:] + [1]):
|
||||
|
||||
if approx_in_range(t, oa1, oa2):
|
||||
if oa1 == oa2:
|
||||
return t_map_int + ia2
|
||||
else:
|
||||
return t_map_int + ia1 + (ia2 - ia1) * ((t - oa1) / (oa2 - oa1))
|
||||
|
||||
|
||||
def do_spiral(self, t1, t2, r1=None, r2=None):
|
||||
print(f' {t1=:.5f} {t2=:.5f} {r1=:.2f} {r2=:.2f}')
|
||||
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
|
||||
|
||||
def r_interpolate(t):
|
||||
t = max(t1, min(t2, t)) # Clip to start/end of spiral
|
||||
f = (t - t1) / (t2 - t1)
|
||||
return r1 + (r2 - r1) * f
|
||||
|
||||
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_map = []
|
||||
circumferences = []
|
||||
n = 100
|
||||
radius_steps = [r1 + (r2 - r1) * i/(n-1) for i in range(n)]
|
||||
angle_steps = [t1 + (t2 - t1) * i/(n-1) for i in range(n)]
|
||||
for r in radius_steps:
|
||||
_ic_arcs, circumference = self.map_circumference(r)
|
||||
circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(circumference))
|
||||
circumferences.append(circumference_sum)
|
||||
|
||||
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 = []
|
||||
for p1, p2 in edge_cycle(inner_circumference):
|
||||
edge_angle = math.dist(p1, p2) / inner_circumference_sum
|
||||
point_angles.append(angle)
|
||||
angle = 0
|
||||
point_angles = [0]
|
||||
for p1, p2 in edge_cycle(circumference):
|
||||
edge_angle = math.dist(p1, p2) / circumference_sum
|
||||
angle += edge_angle
|
||||
point_angles += [a+1 for a in point_angles]
|
||||
point_angles += [point_angles[0] + 2]
|
||||
point_angles.append(angle)
|
||||
|
||||
angle_map.append(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
|
||||
for r, t, point_angles in zip(radius_steps, angle_steps, angle_map):
|
||||
for (p1, p2), (tp1, tp2) in zip(self.poly_edges, itertools.pairwise(point_angles + point_angles[:1])):
|
||||
_arc, p1_proj = self.project_arc(p1, r)
|
||||
_arc, p2_proj = self.project_arc(p2, r)
|
||||
|
||||
if approx_in_range(t1, tp1, tp2):
|
||||
_arc, p2_proj_r1 = self.project_arc(p2, r1)
|
||||
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)
|
||||
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))
|
||||
if approx_in_range(t%1, tp1, tp2):
|
||||
yield interpolate(p1_proj, p2_proj, t%1, tp1, tp2), r
|
||||
|
||||
def dump_to_pdf(self, filename):
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
with PdfPages(filename) as pdf:
|
||||
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, '-', color='black', linewidth=.5, label='Polygon')
|
||||
# polygon outliner
|
||||
poly_x = [p[0] for p in self.poly] + [self.poly[0][0]]
|
||||
poly_y = [p[1] for p in self.poly] + [self.poly[0][1]]
|
||||
ax.plot(poly_x, poly_y, 'b-', linewidth=2, label='Polygon')
|
||||
ax.plot(poly_x, poly_y, 'bo', markersize=4)
|
||||
|
||||
# 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 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 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)
|
||||
# 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
8
uv.lock
generated
8
uv.lock
generated
|
|
@ -569,7 +569,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "kicoil"
|
||||
version = "0.11.1"
|
||||
version = "0.10.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
|
|
@ -585,7 +585,6 @@ dependencies = [
|
|||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ipykernel" },
|
||||
{ name = "matplotlib" },
|
||||
]
|
||||
gds = [
|
||||
{ name = "gdstk" },
|
||||
|
|
@ -608,10 +607,7 @@ requires-dist = [
|
|||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "ipykernel", specifier = ">=7.1.0" },
|
||||
{ name = "matplotlib", specifier = ">=3.10.8" },
|
||||
]
|
||||
dev = [{ name = "ipykernel", specifier = ">=7.1.0" }]
|
||||
gds = [{ name = "gdstk" }]
|
||||
gui = [
|
||||
{ name = "cairosvg" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue