Arbitrary shape WIP
This commit is contained in:
parent
257987da90
commit
0d44db1398
3 changed files with 170 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue