The CGAL WASM build crashes for some simple n-gons

This commit is contained in:
jaseg 2025-12-15 15:31:47 +01:00
parent 400cd9582d
commit 6157ce4983
3 changed files with 106 additions and 11 deletions

View file

@ -26,7 +26,7 @@ import math
import click
from gerbonara.layers import LayerStack
from .geometry import PlanarInductor, divisors, CircleShape, SectorShape, StarShape, SVGShape
from .geometry import PlanarInductor, divisors, CircleShape, SectorShape, StarShape, SVGShape, RectangleShape, RegularPolygonShape
from .kicad import footprint_to_board
from .svg import make_transparent_svg
@ -170,6 +170,28 @@ def trapezoid(ctx, outfile, **kwargs):
ctx.obj['write'](shape, outfile)
@cli.command()
@click.option('--width', type=float, default=50, help='Width [mm]')
@click.option('--height', type=float, default=40, help='Height [mm]')
@click.option('--annular-width', type=float, default=10, help='Width of the trace area on the outside of the shape [mm]')
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
@click.pass_context
def rectangle(ctx, outfile, **kwargs):
shape = RectangleShape(**kwargs)
ctx.obj['write'](shape, outfile)
@cli.command()
@click.option('--diameter', type=float, default=50, help='Width [mm]')
@click.option('-n', '--corners', type=int, default=8, help='Number of corners')
@click.option('--annular-width', type=float, default=10, help='Width of the trace area on the outside of the shape [mm]')
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
@click.pass_context
def regular_polygon(ctx, outfile, **kwargs):
shape = RegularPolygonShape(**kwargs)
ctx.obj['write'](shape, outfile)
@cli.command()
@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]')
@click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]')

View file

@ -195,11 +195,12 @@ class TrapezoidShape(OffsetShape):
height: float
offset: float
annular_width: float
arc_tolerance: float = 0.05 # mm
polygon: list = field(init=False)
def __post_init__(self):
w, h, d = self.width, self.height, self.offset
self.polygon = [(w/2-d, 0), (w/2, h), (-w/2, h), (-w/2+d, 0)]
self.polygon = [(w/2-d, -h/2), (w/2, h/2), (-w/2, h/2), (-w/2+d, -h/2)]
super().__post_init__()
@property
@ -211,6 +212,27 @@ class TrapezoidShape(OffsetShape):
return f'{self.width:.2f} x {self.height:.2f} mm, {self.offset:.2f} mm offset isosceles trapezoidal'
@dataclass
class RectangleShape(OffsetShape):
width: float
height: float
annular_width: float
arc_tolerance: float = 0.05 # mm
polygon: list = field(init=False)
def __post_init__(self):
w, h = self.width, self.height
self.polygon = [(w/2, -h/2), (w/2, h/2), (-w/2, h/2), (-w/2, -h/2)]
super().__post_init__()
@property
def slug(self):
return f'rectangle_{self.width:.2f}x{self.height:.2f}'
@property
def desc(self):
return f'{self.width:.2f} x {self.height:.2f} mm rectangle'
@dataclass
class SectorShape(OffsetShape):
inner_diameter: float
@ -265,6 +287,32 @@ class StarShape(OffsetShape):
purpose = ', for demonic purposes' if self.points == 5 else ''
return f'{self.outer_diameter:.2f} x {self.inner_diameter:.2f} mm star shape{purpose}'
@dataclass
class RegularPolygonShape(OffsetShape):
diameter: float
annular_width: float
corners: int = 8
arc_tolerance: float = 0.05 # mm
polygon: list = field(init=False)
def __post_init__(self):
# center on y axis
pt = lambda r, a: (-r*sin(a), r*cos(a))
circle = lambda r, n, phase: [pt(r, (i + phase)*2*pi/n) for i in range(n)]
self.polygon = list(circle(self.diameter/2, self.corners, 0))
print(self.polygon)
super().__post_init__()
@property
def slug(self):
return f'regular_{self.corners}gon_{self.diameter:.2f}'
@property
def desc(self):
return f'{self.diameter:.2f} mm diameter {self.corners} corner regular polygon'
@dataclass
class SVGShape(OffsetShape):
filename: str
@ -282,6 +330,16 @@ class SVGShape(OffsetShape):
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]
self.polygon = coords
super().__post_init__()
@ -331,7 +389,6 @@ class PlanarInductor():
alpha2 = atan((self.shape.outer_radius - self.shape.inner_radius) / self.sweeping_angle / c2)
alpha = (alpha1+alpha2)/2
self.projected_spiral_pitch = self.spiral_pitch*cos(alpha)
print(self.shape.inner_radius, self.shape.outer_radius, self.spiral_pitch, self.turns_per_layer)
if self.layers == 1 and self.twists > 1:
warnings.warn('Warning: Twists set to a value other than 1, but single-layer mode is enabled. The twists value will be ignored.')

View file

@ -45,6 +45,29 @@ def edge_cycle(points):
return itertools.pairwise(itertools.chain(points, points[:1]))
def polygon_is_clockwise(points):
(x1, y1, i), *_rest = sorted((x, y, i) for i, (x, y) in enumerate(points))
x0, y0 = points[(i-1)%len(points)]
x2, y2 = points[(i+1)%len(points)]
det = (x0*y1 + x1*y2 + x2*y0) - (x2*y1 + x1*y0 + x0*y2)
return det < 0
def polygon_center_of_mass(polygon):
# https://en.wikipedia.org/wiki/Centroid
total_x, total_y = 0, 0
area = 0
for (x1, y1), (x2, y2) in edge_cycle(polygon):
diff = (x1*y2 - x2*y1)
total_x += (x1 + x2) * diff
total_y += (y1 + y2) * diff
area += diff
area /= 2
total_x /= 6*area
total_y /= 6*area
return total_x, total_y
class WasmApp:
def __init__(self, wasm_filename, cachedir="kicoil"):
module_binary = importlib.resources.read_binary(__package__, wasm_filename)
@ -103,14 +126,6 @@ class SkeletonNode:
return self.x, self.y
def polygon_is_clockwise(points):
x1, y1, i = sorted((x, y, i) for x, y in points)
x0, y0 = points[(i-1)%len(points)]
x2, y2 = points[(i+1)%len(points)]
det = (x1*y2 + x2*y3 + x3*y1) - (x3*y2 + x2*y1 + x1*y3)
return det < 0
skeleton_wasm = WasmApp('skeleton.wasm')
def compute_skeleton(exterior):
@ -119,6 +134,7 @@ def compute_skeleton(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)
rc, data = skeleton_wasm.run(input_data)