diff options
Diffstat (limited to 'gerbonara/graphic_primitives.py')
-rw-r--r-- | gerbonara/graphic_primitives.py | 102 |
1 files changed, 84 insertions, 18 deletions
diff --git a/gerbonara/graphic_primitives.py b/gerbonara/graphic_primitives.py index ea8fd9f..e136efb 100644 --- a/gerbonara/graphic_primitives.py +++ b/gerbonara/graphic_primitives.py @@ -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 |