diff --git a/cgal_skeleton_core/skeleton_wrapper.cpp b/cgal_skeleton_core/skeleton_wrapper.cpp index 78d7561..da760d3 100644 --- a/cgal_skeleton_core/skeleton_wrapper.cpp +++ b/cgal_skeleton_core/skeleton_wrapper.cpp @@ -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()); diff --git a/src/kicoil/geometry.py b/src/kicoil/geometry.py index 0cace69..a30933c 100644 --- a/src/kicoil/geometry.py +++ b/src/kicoil/geometry.py @@ -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 @@ -329,17 +338,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__() diff --git a/src/kicoil/skeletonator.py b/src/kicoil/skeletonator.py index a3ce3d4..bad6c63 100644 --- a/src/kicoil/skeletonator.py +++ b/src/kicoil/skeletonator.py @@ -109,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() @@ -308,53 +308,51 @@ class Skeletonator: def dump_to_pdf(self, filename): import matplotlib.pyplot as plt - from matplotlib.backends.backend_pdf import PdfPages - - with PdfPages(filename) as pdf: - fig, ax = plt.subplots(figsize=(10, 10)) + + 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 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], '-', 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], '-', 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, '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, '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) - 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 + 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') + 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() - pdf.savefig(fig, bbox_inches='tight') - plt.close(fig) \ No newline at end of file + 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) \ No newline at end of file