Arbitrary shape WIP

This commit is contained in:
jaseg 2025-12-13 15:20:05 +01:00
parent 257987da90
commit 0d44db1398
3 changed files with 170 additions and 11 deletions

View file

@ -69,6 +69,8 @@ def print_valid_twists(ctx, param, value):
@click.pass_context
def cli(ctx, footprint_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

View file

@ -287,11 +287,11 @@ class SVGShape(OffsetShape):
@property
def slug(self):
return f'svg_{self.outer_diameter:.2f}x{self.inner_diameter:.2f}'
return f'svg_{len(self.polygon)}n'
@property
def desc(self):
return f'{self.outer_diameter:.2f} x {self.inner_diameter:.2f} mm imported SVG shape'
return f'{len(self.polygon)} node imported SVG shape'
@dataclass
class PlanarInductor():
@ -358,13 +358,13 @@ class PlanarInductor():
if self.trace_width is None:
if round(self.clearance, 3) > round(self.projected_spiral_pitch, 3):
raise ValueError(f'Error: Given clearance of {clearance:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
warnings.warn(f'Error: Given clearance of {clearance:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
self.trace_width = self.projected_spiral_pitch - self.clearance
self.logger.info(f'Calculated trace width for {self.clearance:.2f} mm clearance is {self.trace_width:.2f} mm.')
elif self.clearance is None:
if round(self.trace_width, 2) > round(self.projected_spiral_pitch, 2):
raise ValueError(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.')
warnings.warn(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.')
self.clearance = self.projected_spiral_pitch - self.trace_width
self.logger.info(f'Calculated clearance for {self.trace_width:.2f} mm trace width is {self.clearance:.2f} mm.')

View file

@ -1,7 +1,10 @@
import math
import itertools
from py_straight_skeleton import compute_skeleton
import subprocess
import os
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
def interpolate(p1, p2, t, t_start=0, t_end=1):
@ -33,12 +36,113 @@ def edge_cycle(points):
return itertools.pairwise(itertools.chain(points, points[:1]))
class SkeletonNode:
"""Wrapper class for skeleton nodes to match py_straight_skeleton interface"""
def __init__(self, x, y, time):
self.position = type('Position', (), {'x': x, 'y': y})()
self.time = time
self.x = x
self.y = y
def __hash__(self):
return hash((self.x, self.y, self.time))
def __eq__(self, other):
return (self.x, self.y, self.time) == (other.x, other.y, other.time)
def __repr__(self):
return f'SkeletonNode({self.x}, {self.y}, t={self.time})'
class SkeletonWrapper:
"""Wrapper class for skeleton to match py_straight_skeleton interface"""
def __init__(self, nodes, edges):
self.nodes = nodes
self.edges = edges
def arc_iterator(self):
"""Iterate through skeleton edges as node pairs"""
return iter(self.edges)
def compute_skeleton_cli(exterior, holes=None):
"""
Compute straight skeleton using the CLI program instead of py_straight_skeleton.
Args:
exterior: List of (x, y) tuples representing the polygon vertices
holes: Not supported in this implementation
Returns:
SkeletonWrapper object compatible with py_straight_skeleton interface
"""
if holes:
raise NotImplementedError("Holes are not supported in CLI implementation")
# Find the skeleton_cli binary
# Look in project root directory
cli_path = Path(__file__).parent.parent.parent / 'skeleton_cli'
if not cli_path.exists():
raise FileNotFoundError(f"skeleton_cli binary not found at {cli_path}")
# Prepare input: one point per line
points_deduplicated = []
for p1, p2 in edge_cycle(exterior):
if p2 != p1:
points_deduplicated.append(p1)
input_data = '\n'.join(f'{x} {y}' for x, y in points_deduplicated)
Path('/tmp/debug.txt').write_text(input_data)
# Run the CLI program
try:
result = subprocess.run(
[str(cli_path)],
input=input_data,
capture_output=True,
text=True,
check=True
)
except subprocess.CalledProcessError as e:
raise ValueError(f'Error computing polygon straight skeleton. CGAL says: {e.stdout.rstrip()}\n{e.stderr.rstrip()}')
# Parse output: each line is "x1 y1 x2 y2 t1 t2"
node_dict = {} # Map (x, y, t) to SkeletonNode
edges = []
for line in result.stdout.strip().split('\n'):
if not line:
continue
parts = line.split()
if len(parts) != 6:
continue
x1, y1, x2, y2, t1, t2 = map(float, parts)
# Create or get nodes
key1 = (x1, y1, t1)
key2 = (x2, y2, t2)
if key1 not in node_dict:
node_dict[key1] = SkeletonNode(x1, y1, t1)
if key2 not in node_dict:
node_dict[key2] = SkeletonNode(x2, y2, t2)
node1 = node_dict[key1]
node2 = node_dict[key2]
edges.append((node1, node2))
nodes = list(node_dict.values())
return SkeletonWrapper(nodes, edges)
class Skeletonator:
def __init__(self, poly):
self.poly = poly
self.poly_edges = list(zip(poly, poly[1:] + poly[:1]))
self.circumference = sum(math.dist(a, b) for a, b in self.poly_edges)
self.skeleton = compute_skeleton(exterior=poly, holes=[])
self.skeleton = compute_skeleton_cli(exterior=poly, holes=[])
self.arc_map = {}
self.divergent = set()
self.radius = max(n.time for n in self.skeleton.nodes)
@ -48,12 +152,24 @@ class Skeletonator:
self.divergent.add(n1)
self.min_radius = min(n1.time, self.radius)
self.arc_map[n1] = n2
self.node_map = dict(zip(poly, self.skeleton.nodes))
coord_map = {}
for n in self.skeleton.nodes:
p = (round(n.position.x, 6), round(n.position.y, 6))
coord_map[p] = n
self.node_map = {}
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')
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
@ -142,8 +258,49 @@ class Skeletonator:
_arc, p2_proj = self.project_arc(p2, rp2)
if approx_in_range(t1, tp1, tp2):
yield interpolate(p1_proj, p2_proj, t1, tp1, tp2), r_ref
_arc, p2_proj_r1 = self.project_arc(p2, r1)
yield interpolate(p1_proj, p2_proj_r1, t1, tp1, tp2), r_ref
if approx_in_range(t2, tp1, tp2):
yield interpolate(p1_proj, p2_proj, t2, tp1, tp2), r_ref
_arc, p1_proj_r2 = self.project_arc(p1, r2)
yield interpolate(p1_proj_r2, p2_proj, t2, tp1, tp2), r_ref
elif approx_in_range(tp2, t1, t2):
yield p2_proj, r_ref
yield p2_proj, r_ref
def dump_to_pdf(self, filename):
"""
Dump the polygon and its computed skeleton to a PDF file using matplotlib.
"""
with PdfPages(filename) as pdf:
fig, ax = plt.subplots(figsize=(10, 10))
# Plot the polygon
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)
# Plot the skeleton edges
for node1, node2 in self.skeleton.arc_iterator():
ax.plot([node1.x, node2.x], [node1.y, node2.y], 'r-', linewidth=1, alpha=0.7)
# Plot skeleton nodes
for node in self.skeleton.nodes:
ax.plot(node.x, node.y, 'ro', markersize=3, alpha=0.5)
# Plot divergent nodes with a different marker
for node in self.divergent:
ax.plot(node.x, node.y, 'go', markersize=6, label='Divergent' if node == list(self.divergent)[0] else '')
for node in self.skeleton.nodes:
if node not in self.arc_map and node not in self.divergent:
ax.plot(node.x, node.y, 'o', color='magenta', markersize=6, label='Divergent' if self.divergent and node == list(self.divergent)[0] else '')
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')
pdf.savefig(fig, bbox_inches='tight')
plt.close(fig)