The CGAL WASM build crashes for some simple n-gons
This commit is contained in:
parent
400cd9582d
commit
6157ce4983
3 changed files with 106 additions and 11 deletions
|
|
@ -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]')
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue