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 import click
from gerbonara.layers import LayerStack 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 .kicad import footprint_to_board
from .svg import make_transparent_svg from .svg import make_transparent_svg
@ -170,6 +170,28 @@ def trapezoid(ctx, outfile, **kwargs):
ctx.obj['write'](shape, outfile) 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() @cli.command()
@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]') @click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]')
@click.option('--outer-diameter', type=float, default=50, help='Outer 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 height: float
offset: float offset: float
annular_width: float annular_width: float
arc_tolerance: float = 0.05 # mm
polygon: list = field(init=False) polygon: list = field(init=False)
def __post_init__(self): def __post_init__(self):
w, h, d = self.width, self.height, self.offset 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__() super().__post_init__()
@property @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' 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 @dataclass
class SectorShape(OffsetShape): class SectorShape(OffsetShape):
inner_diameter: float inner_diameter: float
@ -265,6 +287,32 @@ class StarShape(OffsetShape):
purpose = ', for demonic purposes' if self.points == 5 else '' purpose = ', for demonic purposes' if self.points == 5 else ''
return f'{self.outer_diameter:.2f} x {self.inner_diameter:.2f} mm star shape{purpose}' 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 @dataclass
class SVGShape(OffsetShape): class SVGShape(OffsetShape):
filename: str filename: str
@ -282,6 +330,16 @@ class SVGShape(OffsetShape):
d = d.strip('MmZ ').replace(',', 'L') d = d.strip('MmZ ').replace(',', 'L')
coord_pairs = d.split('L') coord_pairs = d.split('L')
coords = list(reversed([tuple(map(float, pair.split())) for pair in coord_pairs])) 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 self.polygon = coords
super().__post_init__() super().__post_init__()
@ -331,7 +389,6 @@ class PlanarInductor():
alpha2 = atan((self.shape.outer_radius - self.shape.inner_radius) / self.sweeping_angle / c2) alpha2 = atan((self.shape.outer_radius - self.shape.inner_radius) / self.sweeping_angle / c2)
alpha = (alpha1+alpha2)/2 alpha = (alpha1+alpha2)/2
self.projected_spiral_pitch = self.spiral_pitch*cos(alpha) 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: 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.') 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])) 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: class WasmApp:
def __init__(self, wasm_filename, cachedir="kicoil"): def __init__(self, wasm_filename, cachedir="kicoil"):
module_binary = importlib.resources.read_binary(__package__, wasm_filename) module_binary = importlib.resources.read_binary(__package__, wasm_filename)
@ -103,14 +126,6 @@ class SkeletonNode:
return self.x, self.y 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') skeleton_wasm = WasmApp('skeleton.wasm')
def compute_skeleton(exterior): def compute_skeleton(exterior):
@ -119,6 +134,7 @@ def compute_skeleton(exterior):
if p2 != p1: if p2 != p1:
points_deduplicated.append(p1) points_deduplicated.append(p1)
input_data = '\n'.join(f'{x} {y}' for x, y in points_deduplicated) 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) rc, data = skeleton_wasm.run(input_data)