WIP
This commit is contained in:
parent
d2143bdf4d
commit
95da482033
6 changed files with 212 additions and 118 deletions
|
|
@ -48,6 +48,7 @@ Features
|
|||
|
||||
cli
|
||||
api-concepts
|
||||
examples
|
||||
file-api
|
||||
object-api
|
||||
apertures
|
||||
|
|
@ -74,6 +75,8 @@ Then, you are ready to read and write gerber files:
|
|||
w, h = stack.outline.size('mm')
|
||||
print(f'Board size is {w:.1f} mm x {h:.1f} mm')
|
||||
|
||||
You can find some more elaborate examples in this doc's :ref:`Examples section<examples-doc>`.
|
||||
|
||||
Command-Line Interface
|
||||
======================
|
||||
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ class Arc:
|
|||
x2, y2 = self.mid.x-x1, self.mid.y-x2
|
||||
x3, y3 = (self.end.x - x1)/2, (self.end.y - y1)/2
|
||||
clockwise = math.atan2(x2*y3-x3*y2, x2*x3+y2*y3) > 0
|
||||
return arc_bounds(x1, y1, self.end.x, self.end.y, cx-x1, cy-y1, clockwise)
|
||||
return arc_bounds(x1, y1, self.end.x, self.end.y, cx, cy, clockwise)
|
||||
|
||||
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
import math
|
||||
import copy
|
||||
from dataclasses import dataclass, astuple, field, fields
|
||||
from itertools import zip_longest
|
||||
from itertools import zip_longest, pairwise, islice, cycle
|
||||
|
||||
from .utils import MM, InterpMode, to_unit, rotate_point, sum_bounds
|
||||
from .utils import MM, InterpMode, to_unit, rotate_point, sum_bounds, approximate_arc, sweep_angle
|
||||
from . import graphic_primitives as gp
|
||||
from .aperture_macros import primitive as amp
|
||||
|
||||
|
|
@ -278,9 +278,15 @@ class Region(GraphicObject):
|
|||
* A region is always exactly one connected component.
|
||||
* A region must not overlap itself anywhere.
|
||||
* A region cannot have holes.
|
||||
* The last outline point of the region must be equal to the first.
|
||||
|
||||
There is one exception from the last two rules: To emulate a region with a hole in it, *cut-ins* are allowed. At a
|
||||
cut-in, the region is allowed to touch (but never overlap!) itself.
|
||||
|
||||
When ``arc_centers`` is empty, this region has only straight outline segments. When ``arc_centers`` is not empty,
|
||||
the i-th entry defines the i-th outline segment, with a ``None`` entry designating a straight line segment.
|
||||
An arc is defined by a ``(clockwise, (cx, cy))`` tuple, where ``clockwise`` can be ``True`` for a clockwise arc, or
|
||||
``False`` for a counter-clockwise arc. ``cx`` and ``cy`` are the absolute coordinates of the arc's center.
|
||||
"""
|
||||
|
||||
def __init__(self, outline=None, arc_centers=None, *, unit=MM, polarity_dark=True):
|
||||
|
|
@ -304,8 +310,8 @@ class Region(GraphicObject):
|
|||
def _rotate(self, angle, cx=0, cy=0):
|
||||
self.outline = [ gp.rotate_point(x, y, angle, cx, cy) for x, y in self.outline ]
|
||||
self.arc_centers = [
|
||||
(arc[0], gp.rotate_point(*arc[1], angle, cx-p[0], cy-p[1])) if arc else None
|
||||
for p, arc in zip_longest(self.outline, self.arc_centers) ]
|
||||
(arc[0], gp.rotate_point(*arc[1], angle, cx, cy)) if arc else None
|
||||
for arc in self.arc_centers ]
|
||||
|
||||
def _scale(self, factor):
|
||||
self.outline = [ (x*factor, y*factor) for x, y in self.outline ]
|
||||
|
|
@ -322,6 +328,10 @@ class Region(GraphicObject):
|
|||
(x, y+h),
|
||||
], unit=unit)
|
||||
|
||||
@classmethod
|
||||
def from_arc_poly(kls, arc_poly, polarity_dark=True, unit=MM):
|
||||
return kls(arc_poly.outline, arc_poly.arc_centers, polarity_dark=polarity_dark, unit=unit)
|
||||
|
||||
def append(self, obj):
|
||||
if obj.unit != self.unit:
|
||||
obj = obj.converted(self.unit)
|
||||
|
|
@ -331,48 +341,49 @@ class Region(GraphicObject):
|
|||
self.outline.append(obj.p2)
|
||||
|
||||
if isinstance(obj, Arc):
|
||||
self.arc_centers.append((obj.clockwise, obj.center_relative))
|
||||
self.arc_centers.append((obj.clockwise, obj.center))
|
||||
else:
|
||||
self.arc_centers.append(None)
|
||||
|
||||
def close(self):
|
||||
if not self.outline:
|
||||
return
|
||||
def iter_segments(self, tolerance=1e-6):
|
||||
for points, arc in zip_longest(pairwise(self.outline), self.arc_centers):
|
||||
if arc:
|
||||
if points:
|
||||
yield *points, arc
|
||||
else:
|
||||
yield self.outline[-1], self.outline[0], arc
|
||||
return
|
||||
else:
|
||||
if not points:
|
||||
break
|
||||
yield *points, (None, (None, None))
|
||||
|
||||
if self.outline[-1] != self.outline[0]:
|
||||
self.outline.append(self.outline[0])
|
||||
# Close outline if necessary.
|
||||
if math.dist(self.outline[0], self.outline[-1]) > tolerance:
|
||||
yield self.outline[-1], self.outline[0], (None, (None, None))
|
||||
|
||||
def outline_objects(self, aperture=None):
|
||||
for p1, p2, arc in zip_longest(self.outline, self.outline[1:] + self.outline[:1], self.arc_centers):
|
||||
if arc:
|
||||
clockwise, pc = arc
|
||||
yield Arc(*p1, *p2, *pc, clockwise, aperture=aperture, unit=self.unit, polarity_dark=self.polarity_dark)
|
||||
for p1, p2, (clockwise, center) in self.iter_segments():
|
||||
if center:
|
||||
yield Arc(*p1, *p2, *center, clockwise, aperture=aperture, unit=self.unit, polarity_dark=self.polarity_dark)
|
||||
else:
|
||||
yield Line(*p1, *p2, aperture=aperture, unit=self.unit, polarity_dark=self.polarity_dark)
|
||||
|
||||
def _aperture_macro_primitives(self, max_error=1e-2, unit=MM):
|
||||
def _aperture_macro_primitives(self, max_error=1e-2, clip_max_error=True, unit=MM):
|
||||
# unit is only for max_error, the resulting primitives will always be in MM
|
||||
|
||||
if len(self.outline) < 2:
|
||||
return
|
||||
|
||||
points = [self.outline[0]]
|
||||
for p1, p2, arc in zip_longest(self.outline[:-1], self.outline[1:], self.arc_centers):
|
||||
if arc:
|
||||
clockwise, pc = arc
|
||||
#r = math.hypot(*pc) # arc center is relative to p1.
|
||||
#d = math.dist(p1, p2)
|
||||
#err = r - math.sqrt(r**2 - (d/(2*n))**2)
|
||||
#n = math.ceil(1/(2*math.sqrt(r**2 - (r - max_err)**2)/d))
|
||||
arc = Arc(*p1, *p2, *pc, clockwise, unit=self.unit, polarity_dark=self.polarity_dark, aperture=None)
|
||||
for line in arc.approximate(max_error=max_error, unit=unit):
|
||||
points.append(line.p2)
|
||||
|
||||
points = []
|
||||
for p1, p2, (clockwise, center) in self.iter_segments():
|
||||
if center:
|
||||
for p in approximate_arc(*center, *p1, *p2, clockwise,
|
||||
max_error=max_error, clip_max_error=clip_max_error):
|
||||
points.append(p)
|
||||
points.pop()
|
||||
else:
|
||||
points.append(p2)
|
||||
|
||||
if points[-1] != points[0]:
|
||||
points.append(points[0])
|
||||
points.append(p1)
|
||||
|
||||
yield amp.Outline(self.unit, int(self.polarity_dark), len(points)-1, tuple(coord for p in points for coord in p))
|
||||
|
||||
|
|
@ -389,6 +400,9 @@ class Region(GraphicObject):
|
|||
yield gp.ArcPoly(conv_outline, conv_arc, polarity_dark=self.polarity_dark)
|
||||
|
||||
def to_statements(self, gs):
|
||||
if len(self.outline) < 3:
|
||||
return
|
||||
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
yield 'G36*'
|
||||
# Repeat interpolation mode at start of region statement to work around gerbv bug. Without this, gerbv will
|
||||
|
|
@ -398,32 +412,24 @@ class Region(GraphicObject):
|
|||
|
||||
yield from gs.set_current_point(self.outline[0], unit=self.unit)
|
||||
|
||||
for point, arc_center in zip_longest(self.outline[1:], self.arc_centers):
|
||||
if point is None and arc_center is None:
|
||||
for previous_point, point, (clockwise, center) in self.iter_segments():
|
||||
if point is None and center is None:
|
||||
break
|
||||
|
||||
if arc_center is None:
|
||||
yield from gs.set_interpolation_mode(InterpMode.LINEAR)
|
||||
x = gs.file_settings.write_gerber_value(point[0], self.unit)
|
||||
y = gs.file_settings.write_gerber_value(point[1], self.unit)
|
||||
|
||||
x = gs.file_settings.write_gerber_value(point[0], self.unit)
|
||||
y = gs.file_settings.write_gerber_value(point[1], self.unit)
|
||||
if clockwise is None:
|
||||
yield from gs.set_interpolation_mode(InterpMode.LINEAR)
|
||||
yield f'X{x}Y{y}D01*'
|
||||
|
||||
gs.update_point(*point, unit=self.unit)
|
||||
|
||||
else:
|
||||
clockwise, (cx, cy) = arc_center
|
||||
x2, y2 = point
|
||||
yield from gs.set_interpolation_mode(InterpMode.CIRCULAR_CW if clockwise else InterpMode.CIRCULAR_CCW)
|
||||
|
||||
x = gs.file_settings.write_gerber_value(x2, self.unit)
|
||||
y = gs.file_settings.write_gerber_value(y2, self.unit)
|
||||
# TODO are these coordinates absolute or relative now?!
|
||||
i = gs.file_settings.write_gerber_value(cx, self.unit)
|
||||
j = gs.file_settings.write_gerber_value(cy, self.unit)
|
||||
i = gs.file_settings.write_gerber_value(center[0]-previous_point[0], self.unit)
|
||||
j = gs.file_settings.write_gerber_value(center[1]-previous_point[1], self.unit)
|
||||
yield f'X{x}Y{y}I{i}J{j}D01*'
|
||||
|
||||
gs.update_point(x2, y2, unit=self.unit)
|
||||
gs.update_point(*point, unit=self.unit)
|
||||
|
||||
yield 'G37*'
|
||||
|
||||
|
|
@ -605,22 +611,8 @@ class Arc(GraphicObject):
|
|||
:returns: Angle in clockwise radian between ``0`` and ``2*math.pi``
|
||||
:rtype: float
|
||||
"""
|
||||
cx, cy = self.cx + self.x1, self.cy + self.y1
|
||||
x1, y1 = self.x1 - cx, self.y1 - cy
|
||||
x2, y2 = self.x2 - cx, self.y2 - cy
|
||||
|
||||
a1, a2 = math.atan2(y1, x1), math.atan2(y2, x2)
|
||||
f = abs(a2 - a1)
|
||||
if not self.clockwise:
|
||||
if a2 > a1:
|
||||
return a2 - a1
|
||||
else:
|
||||
return 2*math.pi - abs(a2 - a1)
|
||||
else:
|
||||
if a1 > a2:
|
||||
return a1 - a2
|
||||
else:
|
||||
return 2*math.pi - abs(a1 - a2)
|
||||
return sweep_angle(self.cx+self.x1, self.cy+self.y1, self.x1, self.y1, self.x2, self.y2, self.clockwise)
|
||||
|
||||
@property
|
||||
def p1(self):
|
||||
|
|
@ -677,34 +669,16 @@ class Arc(GraphicObject):
|
|||
:returns: list of :py:class:`~.graphic_objects.Line` instances.
|
||||
:rtype: list
|
||||
"""
|
||||
# TODO the max_angle calculation below is a bit off -- we over-estimate the error, and thus produce finer
|
||||
# results than necessary. Fix this.
|
||||
|
||||
r = math.hypot(self.cx, self.cy)
|
||||
|
||||
max_error = self.unit(max_error, unit)
|
||||
if clip_max_error:
|
||||
# 1 - math.sqrt(1 - 0.5*math.sqrt(2))
|
||||
max_error = min(max_error, r*0.4588038998538031)
|
||||
|
||||
elif max_error >= r:
|
||||
return [Line(*self.p1, *self.p2, aperture=self.aperture, polarity_dark=self.polarity_dark, unit=self.unit)]
|
||||
|
||||
# see https://www.mathopenref.com/sagitta.html
|
||||
l = math.sqrt(r**2 - (r - max_error)**2)
|
||||
|
||||
angle_max = math.asin(l/r)
|
||||
sweep_angle = self.sweep_angle()
|
||||
num_segments = math.ceil(sweep_angle / angle_max)
|
||||
angle = sweep_angle / num_segments
|
||||
|
||||
if not self.clockwise:
|
||||
angle = -angle
|
||||
|
||||
cx, cy = self.center
|
||||
points = [ rotate_point(self.x1, self.y1, i*angle, cx, cy) for i in range(num_segments + 1) ]
|
||||
return [ Line(*p1, *p2, aperture=self.aperture, polarity_dark=self.polarity_dark, unit=self.unit)
|
||||
for p1, p2 in zip(points[0::], points[1::]) ]
|
||||
return [Line(*p1, *p2, aperture=self.aperture, polarity_dark=self.polarity_dark, unit=self.unit)
|
||||
for p1, p2 in pairwise(approximate_arc(
|
||||
self.cx+self.x1, self.cy+self.y1,
|
||||
self.x1, self.y1,
|
||||
self.x2, self.y2,
|
||||
self.clockwise,
|
||||
max_error=max_error,
|
||||
clip_max_error=clip_max_error))]
|
||||
|
||||
def _rotate(self, rotation, cx=0, cy=0):
|
||||
# rotate center first since we need old x1, y1 here
|
||||
|
|
@ -726,7 +700,7 @@ class Arc(GraphicObject):
|
|||
w = self.aperture.equivalent_width(unit) if self.aperture else 0
|
||||
return gp.Arc(x1=conv.x1, y1=conv.y1,
|
||||
x2=conv.x2, y2=conv.y2,
|
||||
cx=conv.cx, cy=conv.cy,
|
||||
cx=conv.cx+conv.x1, cy=conv.cy+conv.y1,
|
||||
clockwise=self.clockwise,
|
||||
width=w,
|
||||
polarity_dark=self.polarity_dark)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import math
|
||||
import itertools
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from dataclasses import dataclass, replace, field
|
||||
|
||||
from .utils import *
|
||||
|
||||
|
|
@ -79,6 +79,10 @@ class Circle(GraphicPrimitive):
|
|||
color = fg if self.polarity_dark else bg
|
||||
return tag('circle', cx=prec(self.x), cy=prec(self.y), r=prec(self.r), fill=color)
|
||||
|
||||
def to_arc_poly(self):
|
||||
return ArcPoly([(self.x-self.r, self.y), (self.x+self.r, self.y)],
|
||||
[(True, (self.x, self.y)), (True, (self.x, self.y))])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArcPoly(GraphicPrimitive):
|
||||
|
|
@ -88,28 +92,51 @@ class ArcPoly(GraphicPrimitive):
|
|||
#: connected.
|
||||
outline : list
|
||||
#: Must be either None (all segments are straight lines) or same length as outline.
|
||||
#: Straight line segments have None entry.
|
||||
arc_centers : list = None
|
||||
#: Straight line segments have None entry. Arc segments have (clockwise, (cx, cy)) tuple with cx, cy being absolute
|
||||
#: coords.
|
||||
arc_centers : list = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def segments(self):
|
||||
""" Return an iterator through all *segments* of this polygon. For each outline segment (line or arc), this
|
||||
iterator will yield a ``(p1, p2, center)`` tuple. If the segment is a straight line, ``center`` will be
|
||||
``None``.
|
||||
iterator will yield a ``(p1, p2, (clockwise, center))`` tuple. If the segment is a straight line, ``clockwise``
|
||||
will be ``None``.
|
||||
"""
|
||||
ol = self.outline
|
||||
return itertools.zip_longest(ol, ol[1:] + [ol[0]], self.arc_centers or [])
|
||||
for points, arc in itertools.zip_longest(itertools.pairwise(self.outline), self.arc_centers):
|
||||
if arc:
|
||||
if points:
|
||||
yield *points, arc
|
||||
else:
|
||||
yield self.outline[-1], self.outline[0], arc
|
||||
return
|
||||
else:
|
||||
if not points:
|
||||
break
|
||||
yield *points, (None, (None, None))
|
||||
|
||||
# Close outline if necessary.
|
||||
if math.dist(self.outline[0], self.outline[-1]) > 1e-6:
|
||||
yield self.outline[-1], self.outline[0], (None, (None, None))
|
||||
|
||||
def approximate_arcs(self, max_error=1e-2, clip_max_error=True):
|
||||
outline = []
|
||||
for p1, p2, (clockwise, center) in self.segments():
|
||||
if clockwise is None:
|
||||
outline.append(p1)
|
||||
else:
|
||||
outline.extend(approximate_arc(cx, cy, x1, y1, x2, y2, clockwise,
|
||||
max_error=max_error, clip_max_error=clip_max_error))
|
||||
outline.pop() # remove arc end point
|
||||
return type(self)(outline)
|
||||
|
||||
def bounding_box(self):
|
||||
bbox = (None, None), (None, None)
|
||||
for (x1, y1), (x2, y2), arc in self.segments:
|
||||
if arc:
|
||||
clockwise, (cx, cy) = arc
|
||||
bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, cx, cy, clockwise))
|
||||
|
||||
else:
|
||||
for (x1, y1), (x2, y2), (clockwise, (cx, cy)) in self.segments:
|
||||
if clockwise is None:
|
||||
line_bounds = (min(x1, x2), min(y1, y2)), (max(x1, x2), max(y1, y2))
|
||||
bbox = add_bounds(bbox, line_bounds)
|
||||
else:
|
||||
bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, cx, cy, clockwise))
|
||||
return bbox
|
||||
|
||||
@classmethod
|
||||
|
|
@ -149,6 +176,9 @@ class ArcPoly(GraphicPrimitive):
|
|||
color = fg if self.polarity_dark else bg
|
||||
return tag('path', d=' '.join(self.path_d()), fill=color)
|
||||
|
||||
def to_arc_poly(self):
|
||||
return self
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Line(GraphicPrimitive):
|
||||
|
|
@ -191,6 +221,24 @@ class Line(GraphicPrimitive):
|
|||
return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} L {float(self.x2):.6} {float(self.y2):.6}',
|
||||
fill='none', stroke=color, stroke_width=str(width))
|
||||
|
||||
def to_arc_poly(self):
|
||||
l = math.dist((self.x1, self.y1), (self.x2, self.y2))
|
||||
dx, dy = self.x2-self.x1, self.y2-self.y1
|
||||
nx, ny = -dy/l, dx/l
|
||||
rx, ry = nx*self.width/2, ny*self.width/2
|
||||
return ArcPoly([
|
||||
(self.x1+rx, self.y1+ry),
|
||||
(self.x1-rx, self.y1-ry),
|
||||
(self.x2-rx, self.y2-ry),
|
||||
(self.x2+rx, self.y2+ry),
|
||||
], [
|
||||
(True, (self.x1, self.y1)),
|
||||
None,
|
||||
(True, (self.x2, self.y2)),
|
||||
None,
|
||||
])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Arc(GraphicPrimitive):
|
||||
""" Circular arc with line width ``width`` going from ``(x1, y1)`` to ``(x2, y2)`` around center at ``(cx, cy)``. """
|
||||
|
|
@ -202,9 +250,9 @@ class Arc(GraphicPrimitive):
|
|||
x2 : float
|
||||
#: End Y coodinate
|
||||
y2 : float
|
||||
#: Center X coordinate relative to ``x1``
|
||||
#: Center X coordinate (absolute)
|
||||
cx : float
|
||||
#: Center Y coordinate relative to ``y1``
|
||||
#: Center Y coordinate (absolute)
|
||||
cy : float
|
||||
#: ``True`` if this arc is clockwise from start to end. Selects between the large arc and the small arc given this
|
||||
#: start, end and center
|
||||
|
|
@ -214,11 +262,10 @@ class Arc(GraphicPrimitive):
|
|||
|
||||
@property
|
||||
def is_circle(self):
|
||||
return math.isclose(self.x1, self.x2) and math.isclose(self.y1, self.y2)
|
||||
return math.isclose(self.x1, self.x2, abs_tol=1e-6) and math.isclose(self.y1, self.y2, abs_tol=1e-6)
|
||||
|
||||
def flip(self):
|
||||
return replace(self, x1=self.x2, y1=self.y2, x2=self.x1, y2=self.y1,
|
||||
cx=(self.x1 + self.cx) - self.x2, cy=(self.y1 + self.cy) - self.y2, clockwise=not self.clockwise)
|
||||
return replace(self, x1=self.x2, y1=self.y2, x2=self.x1, y2=self.y1, clockwise=not self.clockwise)
|
||||
|
||||
def bounding_box(self):
|
||||
r = self.width/2
|
||||
|
|
@ -232,6 +279,25 @@ class Arc(GraphicPrimitive):
|
|||
return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} {arc}',
|
||||
fill='none', stroke=color, stroke_width=width)
|
||||
|
||||
def to_arc_poly(self):
|
||||
r = math.dist((self.x1, self.y1), (self.cx, self.cy))
|
||||
dx1, dy1 = self.x1-self.cx, self.y1-self.cy
|
||||
nx1, ny1 = dx1/r * self.width/2, dy1/r * self.width/2
|
||||
dx2, dy2 = self.x2-self.cx, self.y2-self.cy
|
||||
nx2, ny2 = dx2/r * self.width/2, dy2/r * self.width/2
|
||||
return ArcPoly([
|
||||
(self.x1+nx1, self.y1+nx1),
|
||||
(self.x1-nx1, self.y1-nx1),
|
||||
(self.x2-nx2, self.y2-nx2),
|
||||
(self.x2+nx2, self.y2+nx2),
|
||||
], [
|
||||
(self.clockwise, (self.x1, self.y1)),
|
||||
(self.clockwise, (self.cx, self.cy)),
|
||||
(self.clockwise, (self.x2, self.y2)),
|
||||
(self.clockwise, (self.cx, self.cy)),
|
||||
])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Rectangle(GraphicPrimitive):
|
||||
#: **Center** X coordinate
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ def _parse_path_d(path):
|
|||
cx = mx - nx*nl
|
||||
cy = my - ny*nl
|
||||
|
||||
(min_x, min_y), (max_x, max_y) = arc_bounds(last_x, last_y, ax, ay, cx-last_x, cy-last_y, clockwise=(not sweep))
|
||||
(min_x, min_y), (max_x, max_y) = arc_bounds(last_x, last_y, ax, ay, cx, cy, clockwise=(not sweep))
|
||||
min_x -= sr
|
||||
min_y -= sr
|
||||
max_x += sr
|
||||
|
|
|
|||
|
|
@ -244,6 +244,59 @@ def rotate_point(x, y, angle, cx=0, cy=0):
|
|||
cy + (x - cx) * math.sin(-angle) + (y - cy) * math.cos(-angle))
|
||||
|
||||
|
||||
def sweep_angle(cx, cy, x1, y1, x2, y2, clockwise):
|
||||
""" Calculate absolute sweep angle of arc. This is always a positive number.
|
||||
|
||||
:returns: Angle in clockwise radian between ``0`` and ``2*math.pi``
|
||||
:rtype: float
|
||||
"""
|
||||
x1, y1 = x1-cx, y1-cy
|
||||
x2, y2 = x2-cx, y2-cy
|
||||
|
||||
a1, a2 = math.atan2(y1, x1), math.atan2(y2, x2)
|
||||
f = abs(a2 - a1)
|
||||
if not clockwise:
|
||||
if a2 > a1:
|
||||
return a2 - a1
|
||||
else:
|
||||
return 2*math.pi - abs(a2 - a1)
|
||||
else:
|
||||
if a1 > a2:
|
||||
return a1 - a2
|
||||
else:
|
||||
return 2*math.pi - abs(a1 - a2)
|
||||
|
||||
|
||||
def approximate_arc(cx, cy, x1, y1, x2, y2, clockwise, max_error=1e-2, clip_max_error=True):
|
||||
# TODO the max_angle calculation below is a bit off -- we over-estimate the error, and thus produce finer
|
||||
# results than necessary. Fix this.
|
||||
|
||||
r = math.dist((x1, y1), (cx, cy))
|
||||
|
||||
if clip_max_error:
|
||||
# 1 - math.sqrt(1 - 0.5*math.sqrt(2))
|
||||
max_error = min(max_error, r*0.4588038998538031)
|
||||
|
||||
elif max_error >= r:
|
||||
yield (x1, y1)
|
||||
yield (x2, y2)
|
||||
return
|
||||
|
||||
# see https://www.mathopenref.com/sagitta.html
|
||||
l = math.sqrt(r**2 - (r - max_error)**2)
|
||||
|
||||
angle_max = math.asin(l/r)
|
||||
sweep_angle = sweep_angle(cx, cy, x1, y1, x2, y2, clockwise)
|
||||
num_segments = math.ceil(sweep_angle / angle_max)
|
||||
angle = sweep_angle / num_segments
|
||||
|
||||
if not clockwise:
|
||||
angle = -angle
|
||||
|
||||
for i in range(num_segments + 1):
|
||||
yield rotate_point(x1, y1, i*angle, cx, cy)
|
||||
|
||||
|
||||
def min_none(a, b):
|
||||
""" Like the ``min(..)`` builtin, but if either value is ``None``, returns the other. """
|
||||
if a is None:
|
||||
|
|
@ -340,11 +393,9 @@ def arc_bounds(x1, y1, x2, y2, cx, cy, clockwise):
|
|||
# This solution manages to handle circular arcs given in gerber format (with explicit center and endpoints, plus
|
||||
# sweep direction instead of a format with e.g. angles and radius) without any trigonometric functions (e.g. atan2).
|
||||
#
|
||||
# cx, cy are relative to p1.
|
||||
# cx, cy are in absolute coordinates.
|
||||
|
||||
# Center arc on cx, cy
|
||||
cx += x1
|
||||
cy += y1
|
||||
x1 -= cx
|
||||
x2 -= cx
|
||||
y1 -= cy
|
||||
|
|
@ -461,25 +512,25 @@ def point_line_distance(l1, l2, p):
|
|||
|
||||
|
||||
def svg_arc(old, new, center, clockwise):
|
||||
""" Format an SVG circular arc "A" path data entry given an arc in Gerber notation (i.e. with center relative to
|
||||
first point).
|
||||
""" Format an SVG circular arc "A" path data entry given an arc in Gerber notation (but with center in absolute
|
||||
coordinates).
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
r = float(math.hypot(*center))
|
||||
r = float(math.dist(old, center))
|
||||
# invert sweep flag since the svg y axis is mirrored
|
||||
sweep_flag = int(not clockwise)
|
||||
# In the degenerate case where old == new, we always take the long way around. To represent this "full-circle arc"
|
||||
# in SVG, we have to split it into two.
|
||||
if math.isclose(math.dist(old, new), 0):
|
||||
intermediate = old[0] + 2*center[0], old[1] + 2*center[1]
|
||||
intermediate = old[0] + 2*(center[0]-old[0]), old[1] + 2*(center[1]-old[1])
|
||||
# Note that we have to preserve the sweep flag to avoid causing self-intersections by flipping the direction of
|
||||
# a circular cutin
|
||||
return f'A {r:.6} {r:.6} 0 1 {sweep_flag} {float(intermediate[0]):.6} {float(intermediate[1]):.6} ' +\
|
||||
f'A {r:.6} {r:.6} 0 1 {sweep_flag} {float(new[0]):.6} {float(new[1]):.6}'
|
||||
|
||||
else: # normal case
|
||||
d = point_line_distance(old, new, (old[0]+center[0], old[1]+center[1]))
|
||||
d = point_line_distance(old, new, center[0], center[1])
|
||||
large_arc = int((d < 0) == clockwise)
|
||||
return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {float(new[0]):.6} {float(new[1]):.6}'
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue