Code style improvements

This commit is contained in:
jaseg 2025-12-15 14:26:08 +01:00
parent 2f6f35591e
commit 9e2ae8cdf8
2 changed files with 36 additions and 66 deletions

View file

@ -281,7 +281,7 @@ class SVGShape(OffsetShape):
d = path.attrs['d'] d = path.attrs['d']
d = d.strip('MmZ ').replace(',', 'L') d = d.strip('MmZ ').replace(',', 'L')
coord_pairs = d.split('L') coord_pairs = d.split('L')
coords = [tuple(map(float, pair.split())) for pair in coord_pairs] coords = list(reversed([tuple(map(float, pair.split())) for pair in coord_pairs]))
self.polygon = coords self.polygon = coords
super().__post_init__() super().__post_init__()

View file

@ -2,6 +2,7 @@ import math
import itertools import itertools
import subprocess import subprocess
import os import os
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages from matplotlib.backends.backend_pdf import PdfPages
@ -36,49 +37,26 @@ def edge_cycle(points):
return itertools.pairwise(itertools.chain(points, points[:1])) return itertools.pairwise(itertools.chain(points, points[:1]))
@dataclass(frozen=True)
class SkeletonNode: class SkeletonNode:
"""Wrapper class for skeleton nodes to match py_straight_skeleton interface""" x: float
def __init__(self, x, y, time): y: float
self.position = type('Position', (), {'x': x, 'y': y})() time: float
self.time = time
self.x = x
self.y = y
def __hash__(self): @property
return hash((self.x, self.y, self.time)) def pos(self):
return self.x, self.y
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: def polygon_is_clockwise(points):
"""Wrapper class for skeleton to match py_straight_skeleton interface""" x1, y1, i = sorted((x, y, i) for x, y in points)
def __init__(self, nodes, edges): x0, y0 = points[(i-1)%len(points)]
self.nodes = nodes x2, y2 = points[(i+1)%len(points)]
self.edges = edges det = (x1*y2 + x2*y3 + x3*y1) - (x3*y2 + x2*y1 + x1*y3)
return det < 0
def arc_iterator(self):
"""Iterate through skeleton edges as node pairs"""
return iter(self.edges)
def compute_skeleton_cli(exterior, holes=None): def compute_skeleton_cli(exterior):
"""
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 # Find the skeleton_cli binary
# Look in project root directory # Look in project root directory
cli_path = Path(__file__).parent.parent.parent / 'skeleton_cli' cli_path = Path(__file__).parent.parent.parent / 'skeleton_cli'
@ -134,7 +112,7 @@ def compute_skeleton_cli(exterior, holes=None):
edges.append((node1, node2)) edges.append((node1, node2))
nodes = list(node_dict.values()) nodes = list(node_dict.values())
return SkeletonWrapper(nodes, edges) return nodes, edges
class Skeletonator: class Skeletonator:
@ -142,19 +120,19 @@ class Skeletonator:
self.poly = poly self.poly = poly
self.poly_edges = list(zip(poly, poly[1:] + poly[:1])) 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.circumference = sum(math.dist(a, b) for a, b in self.poly_edges)
self.skeleton = compute_skeleton_cli(exterior=poly, holes=[]) self.skeleton_nodes, self.skeleton_edges = compute_skeleton_cli(exterior=poly, holes=[])
self.arc_map = {} self.arc_map = {}
self.divergent = set() self.divergent = set()
self.radius = max(n.time for n in self.skeleton.nodes) self.radius = max(n.time for n in self.skeleton_nodes)
self.min_radius = self.radius self.min_radius = self.radius
for n1, n2 in self.skeleton.arc_iterator(): for n1, n2 in self.skeleton_edges:
if n1 in self.arc_map: if n1 in self.arc_map:
self.divergent.add(n1) self.divergent.add(n1)
self.min_radius = min(n1.time, self.radius) self.min_radius = min(n1.time, self.radius)
self.arc_map[n1] = n2 self.arc_map[n1] = n2
coord_map = {} coord_map = {}
for n in self.skeleton.nodes: for n in self.skeleton_nodes:
p = (round(n.position.x, 6), round(n.position.y, 6)) p = (round(n.x, 6), round(n.y, 6))
coord_map[p] = n coord_map[p] = n
self.node_map = {} self.node_map = {}
for x, y in poly: for x, y in poly:
@ -178,11 +156,9 @@ class Skeletonator:
for n0, n1 in self.iter_arcs(p): for n0, n1 in self.iter_arcs(p):
if t < 0 or approx_in_range(t, n0.time, n1.time): if t < 0 or approx_in_range(t, n0.time, n1.time):
p0 = (n0.position.x, n0.position.y) return (n0, n1), interpolate(n0.pos, n1.pos, t, n0.time, n1.time)
p1 = (n1.position.x, n1.position.y)
return (n0, n1), interpolate(p0, p1, t, n0.time, n1.time)
else: else:
raise ValueError(f'{r=:.3f} is out of bounds [0, {self.min_radius:.4f}]') raise ValueError(f'{r=:.3f} is out of bounds [0, {self.radius - self.min_radius:.4f}]')
def calc_circumference(self, r): def calc_circumference(self, r):
projected = [self.project_arc(p, r)[1] for p in self.poly] projected = [self.project_arc(p, r)[1] for p in self.poly]
@ -267,33 +243,27 @@ class Skeletonator:
yield p2_proj, r_ref yield p2_proj, r_ref
def dump_to_pdf(self, filename): def dump_to_pdf(self, filename):
"""
Dump the polygon and its computed skeleton to a PDF file using matplotlib.
"""
with PdfPages(filename) as pdf: with PdfPages(filename) as pdf:
fig, ax = plt.subplots(figsize=(10, 10)) fig, ax = plt.subplots(figsize=(10, 10))
# Plot the polygon # polygon outline
poly_x = [p[0] for p in self.poly] + [self.poly[0][0]] 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]] 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, 'b-', linewidth=2, label='Polygon')
ax.plot(poly_x, poly_y, 'bo', markersize=4) ax.plot(poly_x, poly_y, 'bo', markersize=4)
# Plot the skeleton edges # skeleton edges
for node1, node2 in self.skeleton.arc_iterator(): for node1, node2 in self.skeleton_edges:
ax.plot([node1.x, node2.x], [node1.y, node2.y], 'r-', linewidth=1, alpha=0.7) ax.plot([node1.x, node2.x], [node1.y, node2.y], 'r-', linewidth=1, alpha=0.7)
# Plot skeleton nodes # skeleton nodes
for node in self.skeleton.nodes: for n in self.skeleton_nodes:
ax.plot(node.x, node.y, 'ro', markersize=3, alpha=0.5) if n in self.divergent:
ax.plot(n.x, n.y, 'go', markersize=6)
# Plot divergent nodes with a different marker elif n in self.arc_map:
for node in self.divergent: ax.plot(n.x, n.y, 'ro', markersize=3, alpha=0.5)
ax.plot(node.x, node.y, 'go', markersize=6, label='Divergent' if node == list(self.divergent)[0] else '') else:
ax.plot(node.x, node.y, 'o', color='magenta', markersize=6)
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.set_aspect('equal', adjustable='box')
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.3)
@ -301,6 +271,6 @@ class Skeletonator:
ax.set_title(f'Polygon Skeleton (radius: {self.radius:.3f}, min_radius: {self.min_radius:.3f})') ax.set_title(f'Polygon Skeleton (radius: {self.radius:.3f}, min_radius: {self.min_radius:.3f})')
ax.set_xlabel('X') ax.set_xlabel('X')
ax.set_ylabel('Y') ax.set_ylabel('Y')
ax.inver_yaxis()
pdf.savefig(fig, bbox_inches='tight') pdf.savefig(fig, bbox_inches='tight')
plt.close(fig) plt.close(fig)