Add a bunch of 2d to_poly / bounding_box functions (untested)
This commit is contained in:
parent
483f3dd4f8
commit
5885b60f14
4 changed files with 385 additions and 133 deletions
|
|
@ -7,9 +7,12 @@ from .aperture_macros.parse import GenericMacros
|
|||
from . import graphic_primitives as gp
|
||||
|
||||
|
||||
def _flash_hole(self, x, y):
|
||||
def _flash_hole(self, x, y, unit=None):
|
||||
if self.hole_rect_h is not None:
|
||||
return self.primitives(x, y), Rectangle((x, y), (self.hole_dia, self.hole_rect_h), rotation=self.rotation, polarity_dark=False)
|
||||
return [*self.primitives(x, y, unit),
|
||||
Rectangle((x, y),
|
||||
(self.convert(self.hole_dia, unit), self.convert(self.hole_rect_h, unit)),
|
||||
rotation=self.rotation, polarity_dark=False)]
|
||||
else:
|
||||
return self.primitives(x, y), Circle((x, y), self.hole_dia, polarity_dark=False)
|
||||
|
||||
|
|
@ -71,11 +74,10 @@ class Aperture:
|
|||
|
||||
return out
|
||||
|
||||
def flash(self, x, y):
|
||||
return self.primitives(x, y)
|
||||
def flash(self, x, y, unit=None):
|
||||
return self.primitives(x, y, unit)
|
||||
|
||||
@property
|
||||
def equivalent_width(self):
|
||||
def equivalent_width(self, unit=None):
|
||||
raise ValueError('Non-circular aperture used in interpolation statement, line width is not properly defined.')
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
|
|
@ -108,17 +110,16 @@ class CircleAperture(Aperture):
|
|||
hole_rect_h : Length(float) = None
|
||||
rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber
|
||||
|
||||
def primitives(self, x, y, rotation):
|
||||
return [ gp.Circle(x, y, self.diameter/2) ]
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.Circle(x, y, self.convert(self.diameter/2, unit)) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<circle aperture d={self.diameter:.3}>'
|
||||
|
||||
flash = _flash_hole
|
||||
|
||||
@property
|
||||
def equivalent_width(self):
|
||||
return self.diameter
|
||||
def equivalent_width(self, unit=None):
|
||||
return self.convert(self.diameter, unit)
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
offset = self.convert_from(offset, unit)
|
||||
|
|
@ -150,17 +151,16 @@ class RectangleAperture(Aperture):
|
|||
hole_rect_h : Length(float) = None
|
||||
rotation : float = 0 # radians
|
||||
|
||||
def primitives(self, x, y):
|
||||
return [ gp.Rectangle(x, y, self.w, self.h, rotation=self.rotation) ]
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.Rectangle(x, y, self.convert(self.w, unit), self.convert(self.h, unit), rotation=self.rotation) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<rect aperture {self.w:.3}x{self.h:.3}>'
|
||||
|
||||
flash = _flash_hole
|
||||
|
||||
@property
|
||||
def equivalent_width(self):
|
||||
return math.sqrt(self.w**2 + self.h**2)
|
||||
def equivalent_width(self, unit=None):
|
||||
return self.convert(math.sqrt(self.w**2 + self.h**2), unit)
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
offset = self.convert_from(offset, unit)
|
||||
|
|
@ -200,8 +200,8 @@ class ObroundAperture(Aperture):
|
|||
hole_rect_h : Length(float) = None
|
||||
rotation : float = 0
|
||||
|
||||
def primitives(self, x, y):
|
||||
return [ gp.Obround(x, y, self.w, self.h, rotation=self.rotation) ]
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.Obround(x, y, self.convert(self.w, unit), self.convert(self.h, unit), rotation=self.rotation) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<obround aperture {self.w:.3}x{self.h:.3}>'
|
||||
|
|
@ -246,8 +246,8 @@ class PolygonAperture(Aperture):
|
|||
rotation : float = 0
|
||||
hole_dia : Length(float) = None
|
||||
|
||||
def primitives(self, x, y):
|
||||
return [ gp.RegularPolygon(x, y, diameter, n_vertices, rotation=self.rotation) ]
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.RegularPolygon(x, y, self.convert(diameter, unit), n_vertices, rotation=self.rotation) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<{self.n_vertices}-gon aperture d={self.diameter:.3}'
|
||||
|
|
@ -279,16 +279,13 @@ class ApertureMacroInstance(Aperture):
|
|||
parameters : [float]
|
||||
rotation : float = 0
|
||||
|
||||
def __post__init__(self, macro):
|
||||
self._primitives = macro.to_graphic_primitives(parameters)
|
||||
|
||||
@property
|
||||
def gerber_shape_code(self):
|
||||
return self.macro.name
|
||||
|
||||
def primitives(self, x, y):
|
||||
# FIXME return graphical primitives not macro primitives here
|
||||
return [ primitive.with_offset(x, y).rotated(self.rotation, cx=0, cy=0) for primitive in self._primitives ]
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ primitive.with_offset(x, y).rotated(self.rotation, cx=0, cy=0)
|
||||
for primitive in self.macro.to_graphic_primitives(self.parameters, unit=unit) ]
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
return replace(self, macro=self.macro.dilated(offset, unit))
|
||||
|
|
|
|||
|
|
@ -5,29 +5,69 @@ from dataclasses import dataclass, KW_ONLY, astuple, replace
|
|||
from . import graphic_primitives as gp
|
||||
from .gerber_statements import *
|
||||
|
||||
|
||||
def convert(value, src, dst):
|
||||
if src == dst or src is None or dst is None or value is None:
|
||||
return value
|
||||
elif dst == 'mm':
|
||||
return value * 25.4
|
||||
else:
|
||||
return value / 25.4
|
||||
|
||||
class Length:
|
||||
def __init__(self, obj_type):
|
||||
self.type = obj_type
|
||||
|
||||
@dataclass
|
||||
class GerberObject:
|
||||
_ : KW_ONLY
|
||||
polarity_dark : bool = True
|
||||
unit : str = None
|
||||
|
||||
def to_primitives(self):
|
||||
def converted(self, unit):
|
||||
return replace(self,
|
||||
**{
|
||||
f.name: convert(getattr(self, f.name), self.unit, unit)
|
||||
for f in fields(self)
|
||||
})
|
||||
|
||||
def _conv(self, value, unit):
|
||||
return convert(value, src=unit, dst=self.unit)
|
||||
|
||||
def with_offset(self, dx, dy, unit='mm'):
|
||||
dx, dy = self._conv(dx, unit), self._conv(dy, unit)
|
||||
return self._with_offset(dx, dy)
|
||||
|
||||
def rotate(self, rotation, cx=0, cy=0, unit='mm'):
|
||||
cx, cy = self._conv(cx, unit), self._conv(cy, unit)
|
||||
return self._rotate(cx, cy)
|
||||
|
||||
def bounding_box(self, unit=None):
|
||||
bboxes = [ p.bounding_box for p in self.to_primitives(unit) ]
|
||||
min_x = min(min_x for (min_x, _min_y), _ in bboxes)
|
||||
min_y = min(min_y for (_min_x, min_y), _ in bboxes)
|
||||
max_x = max(max_x for _, (max_x, _max_y) in bboxes)
|
||||
max_y = max(max_y for _, (_max_x, max_y) in bboxes)
|
||||
return ((min_x, min_y), (max_x, max_y))
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
@dataclass
|
||||
class Flash(GerberObject):
|
||||
x : float
|
||||
y : float
|
||||
x : Length(float)
|
||||
y : Length(float)
|
||||
aperture : object
|
||||
|
||||
def with_offset(self, dx, dy):
|
||||
def _with_offset(self, dx, dy):
|
||||
return replace(self, x=self.x+dx, y=self.y+dy)
|
||||
|
||||
def rotate(self, rotation, cx=0, cy=0):
|
||||
def _rotate(self, rotation, cx=0, cy=0):
|
||||
self.x, self.y = gp.rotate_point(self.x, self.y, rotation, cx, cy)
|
||||
|
||||
def to_primitives(self):
|
||||
yield from self.aperture.flash(self.x, self.y)
|
||||
def to_primitives(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
yield from self.aperture.flash(conv.x, conv.y, unit)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
@ -48,31 +88,33 @@ class Region(GerberObject):
|
|||
def __bool__(self):
|
||||
return bool(self.poly)
|
||||
|
||||
def with_offset(self, dx, dy):
|
||||
def _with_offset(self, dx, dy):
|
||||
return Region([ (x+dx, y+dy) for x, y in self.poly.outline ],
|
||||
self.poly.arc_centers,
|
||||
polarity_dark=self.polarity_dark,
|
||||
unit=self.unit)
|
||||
|
||||
def rotate(self, angle, cx=0, cy=0):
|
||||
def _rotate(self, angle, cx=0, cy=0):
|
||||
self.poly.outline = [ gp.rotate_point(x, y, angle, cx, cy) for x, y in self.poly.outline ]
|
||||
self.poly.arc_centers = [
|
||||
gp.rotate_point(*center, angle, cx, cy) if center else None
|
||||
for center in self.poly.arc_centers ]
|
||||
(arc[0], gp.rotate_point(*arc[1], angle, cx, cy)) if arc else None
|
||||
for arc in self.poly.arc_centers ]
|
||||
|
||||
def append(self, obj):
|
||||
if obj.unit != self.unit:
|
||||
raise ValueError('Cannot append Polyline with "{obj.unit}" coords to Region with "{self.unit}" coords.')
|
||||
if not self.poly.outline:
|
||||
self.poly.outline.append(obj.p1)
|
||||
self.poly.outline.append(obj.p2)
|
||||
|
||||
if isinstance(obj, Arc):
|
||||
self.poly.arc_centers.append(obj.center)
|
||||
self.poly.arc_centers.append((obj.clockwise, obj.center))
|
||||
else:
|
||||
self.poly.arc_centers.append(None)
|
||||
|
||||
def to_primitives(self):
|
||||
def to_primitives(self, unit=None):
|
||||
self.poly.polarity_dark = polarity_dark
|
||||
yield self.poly
|
||||
yield self.poly.converted(unit)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
@ -87,9 +129,9 @@ class Region(GerberObject):
|
|||
gs.update_point(*point, unit=self.unit)
|
||||
|
||||
else:
|
||||
cx, cy = arc_center
|
||||
clockwise, (cx, cy) = arc_center
|
||||
x2, y2 = point
|
||||
yield from gs.set_interpolation_mode(CircularCCWModeStmt)
|
||||
yield from gs.set_interpolation_mode(CircularCWModeStmt if clockwise else CircularCCWModeStmt)
|
||||
yield InterpolateStmt(x2, y2, cx-x2, cy-y2, unit=self.unit)
|
||||
gs.update_point(x2, y2, unit=self.unit)
|
||||
|
||||
|
|
@ -99,16 +141,16 @@ class Region(GerberObject):
|
|||
@dataclass
|
||||
class Line(GerberObject):
|
||||
# Line with *round* end caps.
|
||||
x1 : float
|
||||
y1 : float
|
||||
x2 : float
|
||||
y2 : float
|
||||
x1 : Length(float)
|
||||
y1 : Length(float)
|
||||
x2 : Length(float)
|
||||
y2 : Length(float)
|
||||
aperture : object
|
||||
|
||||
def with_offset(self, dx, dy):
|
||||
def _with_offset(self, dx, dy):
|
||||
return replace(self, x1=self.x1+dx, y1=self.y1+dy, x2=self.x2+dx, y2=self.y2+dy)
|
||||
|
||||
def rotate(self, rotation, cx=0, cy=0):
|
||||
def _rotate(self, rotation, cx=0, cy=0):
|
||||
self.x1, self.y1 = gp.rotate_point(self.x1, self.y1, rotation, cx, cy)
|
||||
self.x2, self.y2 = gp.rotate_point(self.x2, self.y2, rotation, cx, cy)
|
||||
|
||||
|
|
@ -120,8 +162,9 @@ class Line(GerberObject):
|
|||
def p2(self):
|
||||
return self.x2, self.y2
|
||||
|
||||
def to_primitives(self):
|
||||
yield gp.Line(*self.p1, *self.p2, self.aperture.equivalent_width, polarity_dark=self.polarity_dark)
|
||||
def to_primitives(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
yield gp.Line(*conv.p1, *conv.p2, self.aperture.equivalent_width(unit), polarity_dark=self.polarity_dark)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
@ -134,32 +177,33 @@ class Line(GerberObject):
|
|||
|
||||
@dataclass
|
||||
class Drill(GerberObject):
|
||||
x : float
|
||||
y : float
|
||||
diameter : float
|
||||
x : Length(float)
|
||||
y : Length(float)
|
||||
diameter : Length(float)
|
||||
|
||||
def with_offset(self, dx, dy):
|
||||
def _with_offset(self, dx, dy):
|
||||
return replace(self, x=self.x+dx, y=self.y+dy)
|
||||
|
||||
def rotate(self, angle, cx=0, cy=0):
|
||||
def _rotate(self, angle, cx=0, cy=0):
|
||||
self.x, self.y = gp.rotate_point(self.x, self.y, angle, cx, cy)
|
||||
|
||||
def to_primitives(self):
|
||||
yield gp.Circle(self.x, self.y, self.diameter/2)
|
||||
def to_primitives(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
yield gp.Circle(conv.x, conv.y, conv.diameter/2)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Slot(GerberObject):
|
||||
x1 : float
|
||||
y1 : float
|
||||
x2 : float
|
||||
y2 : float
|
||||
width : float
|
||||
x1 : Length(float)
|
||||
y1 : Length(float)
|
||||
x2 : Length(float)
|
||||
y2 : Length(float)
|
||||
width : Length(float)
|
||||
|
||||
def with_offset(self, dx, dy):
|
||||
def _with_offset(self, dx, dy):
|
||||
return replace(self, x1=self.x1+dx, y1=self.y1+dy, x2=self.x2+dx, y2=self.y2+dy)
|
||||
|
||||
def rotate(self, rotation, cx=0, cy=0):
|
||||
def _rotate(self, rotation, cx=0, cy=0):
|
||||
if cx is None:
|
||||
cx = (self.x1 + self.x2) / 2
|
||||
cy = (self.y1 + self.y2) / 2
|
||||
|
|
@ -174,22 +218,23 @@ class Slot(GerberObject):
|
|||
def p2(self):
|
||||
return self.x2, self.y2
|
||||
|
||||
def to_primitives(self):
|
||||
yield gp.Line(*self.p1, *self.p2, self.width, polarity_dark=self.polarity_dark)
|
||||
def to_primitives(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
yield gp.Line(*conv.p1, *conv.p2, conv.width, polarity_dark=self.polarity_dark)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Arc(GerberObject):
|
||||
x1 : float
|
||||
y1 : float
|
||||
x2 : float
|
||||
y2 : float
|
||||
cx : float
|
||||
cy : float
|
||||
flipped : bool
|
||||
x1 : Length(float)
|
||||
y1 : Length(float)
|
||||
x2 : Length(float)
|
||||
y2 : Length(float)
|
||||
cx : Length(float)
|
||||
cy : Length(float)
|
||||
clockwise : bool
|
||||
aperture : object
|
||||
|
||||
def with_offset(self, dx, dy):
|
||||
def _with_offset(self, dx, dy):
|
||||
return replace(self, x1=self.x1+dx, y1=self.y1+dy, x2=self.x2+dx, y2=self.y2+dy)
|
||||
|
||||
@property
|
||||
|
|
@ -204,15 +249,16 @@ class Arc(GerberObject):
|
|||
def center(self):
|
||||
return self.cx + self.x1, self.cy + self.y1
|
||||
|
||||
def rotate(self, rotation, cx=0, cy=0):
|
||||
def _rotate(self, rotation, cx=0, cy=0):
|
||||
# rotate center first since we need old x1, y1 here
|
||||
new_cx, new_cy = gp.rotate_point(*self.center, rotation, cx, cy)
|
||||
self.x1, self.y1 = gp.rotate_point(self.x1, self.y1, rotation, cx, cy)
|
||||
self.x2, self.y2 = gp.rotate_point(self.x2, self.y2, rotation, cx, cy)
|
||||
self.cx, self.cy = new_cx - self.x1, new_cy - self.y1
|
||||
|
||||
def to_primitives(self):
|
||||
yield gp.Arc(*astuple(self)[:7], width=self.aperture.equivalent_width, polarity_dark=self.polarity_dark)
|
||||
def to_primitives(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
yield gp.Arc(*astuple(conv)[:7], width=self.aperture.equivalent_width(unit), polarity_dark=self.polarity_dark)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from .gerber_statements import *
|
|||
class GraphicPrimitive:
|
||||
_ : KW_ONLY
|
||||
polarity_dark : bool = True
|
||||
unit : str = None
|
||||
|
||||
|
||||
def rotate_point(x, y, angle, cx=0, cy=0):
|
||||
|
|
@ -19,6 +18,26 @@ def rotate_point(x, y, angle, cx=0, cy=0):
|
|||
return (cx + (x - cx) * math.cos(-angle) - (y - cy) * math.sin(-angle),
|
||||
cy + (x - cx) * math.sin(-angle) + (y - cy) * math.cos(-angle))
|
||||
|
||||
def min_none(a, b):
|
||||
if a is None:
|
||||
return b
|
||||
if b is None:
|
||||
return a
|
||||
return min(a, b)
|
||||
|
||||
def max_none(a, b):
|
||||
if a is None:
|
||||
return b
|
||||
if b is None:
|
||||
return a
|
||||
return max(a, b)
|
||||
|
||||
def add_bounds(b1, b2):
|
||||
(min_x_1, min_y_1), (max_x_1, max_y_1) = b1
|
||||
(min_x_2, min_y_2), (max_x_2, max_y_2) = b2
|
||||
min_x, min_y = min_none(min_x_1, min_x_2), min_none(min_y_1, min_y_2)
|
||||
max_x, max_y = max_none(max_x_1, max_x_2), max_none(max_y_1, max_y_2)
|
||||
return ((min_x, min_y), (max_x, max_y))
|
||||
|
||||
@dataclass
|
||||
class Circle(GraphicPrimitive):
|
||||
|
|
@ -26,9 +45,12 @@ class Circle(GraphicPrimitive):
|
|||
y : float
|
||||
r : float # Here, we use radius as common in modern computer graphics, not diameter as gerber uses.
|
||||
|
||||
def bounds(self):
|
||||
def bounding_box(self):
|
||||
return ((self.x-self.r, self.y-self.r), (self.x+self.r, self.y+self.r))
|
||||
|
||||
def to_svg(self):
|
||||
return 'circle', (), dict(cx=x, cy=y, r=r)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Obround(GraphicPrimitive):
|
||||
|
|
@ -38,30 +60,121 @@ class Obround(GraphicPrimitive):
|
|||
h : float
|
||||
rotation : float # radians!
|
||||
|
||||
def decompose(self):
|
||||
''' decompose obround to two circles and one rectangle '''
|
||||
|
||||
cx = self.x + self.w/2
|
||||
cy = self.y + self.h/2
|
||||
|
||||
def to_line(self):
|
||||
if self.w > self.h:
|
||||
x = self.x + self.h/2
|
||||
yield Circle(x, cy, self.h/2)
|
||||
yield Circle(x + self.w, cy, self.h/2)
|
||||
yield Rectangle(x, self.y, self.w - self.h, self.h)
|
||||
|
||||
elif self.h > self.w:
|
||||
y = self.y + self.w/2
|
||||
yield Circle(cx, y, self.w/2)
|
||||
yield Circle(cx, y + self.h, self.w/2)
|
||||
yield Rectangle(self.x, y, self.w, self.h - self.w)
|
||||
|
||||
w, a, b = self.h, self.w, 0
|
||||
else:
|
||||
yield Circle(cx, cy, self.w/2)
|
||||
w, a, b = self.w, 0, self.h
|
||||
return Line(
|
||||
*rotate_point(self.x-a/2, self.y-b/2, self.rotation, self.x, self.y),
|
||||
*rotate_point(self.x+a/2, self.y+b/2, self.rotation, self.x, self.y),
|
||||
w)
|
||||
|
||||
def bounds(self):
|
||||
return ((self.x-self.w/2, self.y-self.h/2), (self.x+self.w/2, self.y+self.h/2))
|
||||
def bounding_box(self):
|
||||
return self.to_line().bounding_box()
|
||||
|
||||
def to_svg(self):
|
||||
return self.to_line().to_svg()
|
||||
|
||||
|
||||
def arc_bounds(x1, y1, x2, y2, cx, cy, clockwise):
|
||||
# This is one of these problems typical for computer geometry where out of nowhere a seemingly simple task just
|
||||
# happens to be anything but in practice.
|
||||
#
|
||||
# Online there are a number of algorithms to be found solving this problem. Often, they solve the more general
|
||||
# problem for elliptic arcs. We can keep things simple here since we only have circular arcs.
|
||||
#
|
||||
# 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).
|
||||
|
||||
# Center arc on cx, cy
|
||||
x1 -= cx
|
||||
x2 -= cx
|
||||
y1 -= cy
|
||||
y2 -= cy
|
||||
clockwise = bool(clockwise) # bool'ify for XOR/XNOR below
|
||||
|
||||
# Calculate radius
|
||||
r = math.sqrt(x1**2 + y1**2)
|
||||
|
||||
# Calculate in which half-planes (north/south, west/east) P1 and P2 lie.
|
||||
# Note that we assume the y axis points upwards, as in Gerber and maths.
|
||||
# SVG has its y axis pointing downwards.
|
||||
p1_west = x1 < 0
|
||||
p1_north = y1 > 0
|
||||
p2_west = x2 < 0
|
||||
p2_north = y2 > 0
|
||||
|
||||
# Calculate bounding box of P1 and P2
|
||||
min_x = min(x1, x2)
|
||||
min_y = min(y1, y2)
|
||||
max_x = max(x1, x2)
|
||||
max_y = max(y1, y2)
|
||||
|
||||
# North
|
||||
# ^
|
||||
# |
|
||||
# |(0,0)
|
||||
# West <-----X-----> East
|
||||
# |
|
||||
# +Y |
|
||||
# ^ v
|
||||
# | South
|
||||
# |
|
||||
# +-----> +X
|
||||
#
|
||||
# Check whether the arc sweeps over any coordinate axes. If it does, add the intersection point to the bounding box.
|
||||
# Note that, since this intersection point is at radius r, it has coordinate e.g. (0, r) for the north intersection.
|
||||
# Since we know that the points lie on either side of the coordinate axis, the '0' coordinate of the intersection
|
||||
# point will not change the bounding box in that axis--only its 'r' coordinate matters. We also know that the
|
||||
# absolute value of that coordinate will be greater than or equal to the old coordinate in that direction since the
|
||||
# intersection with the axis is the point where the full circle is tangent to the AABB. Thus, we can blindly set the
|
||||
# corresponding coordinate of the bounding box without min()/max()'ing first.
|
||||
|
||||
# Handle north/south halfplanes
|
||||
if p1_west != p2_west: # arc starts in west half-plane, ends in east half-plane
|
||||
if p1_west == clockwise: # arc is clockwise west -> east or counter-clockwise east -> west
|
||||
max_y = r # add north to bounding box
|
||||
else: # arc is counter-clockwise west -> east or clockwise east -> west
|
||||
min_y = -r # south
|
||||
else: # Arc starts and ends in same halfplane west/east
|
||||
# Since both points are on the arc (at same radius) in one halfplane, we can use the y coord as a proxy for
|
||||
# angle comparisons.
|
||||
small_arc_is_north_to_south = y1 > y2
|
||||
small_arc_is_clockwise = small_arc_is_north_to_south == p1_west
|
||||
if small_arc_is_clockwise != clockwise:
|
||||
min_y, max_y = -r, r # intersect aabb with both north and south
|
||||
|
||||
# Handle west/east halfplanes
|
||||
if p1_north != p2_north:
|
||||
if p1_north == clockwise:
|
||||
max_x = r # east
|
||||
else:
|
||||
min_x = -r # west
|
||||
else:
|
||||
small_arc_is_west_to_east = x1 < x2
|
||||
small_arc_is_clockwise = small_arc_is_west_to_east == p1_north
|
||||
if small_arc_is_clockwise != clockwise:
|
||||
min_x, max_x = -r, r # intersect aabb with both north and south
|
||||
|
||||
return (min_x+cx, min_y+cy), (max_x+cx, max_y+cy)
|
||||
|
||||
|
||||
def point_distance(a, b):
|
||||
return math.sqrt((b[0] - a[0])**2 + (b[1] - a[1])**2)
|
||||
|
||||
def point_line_distance(l1, l2, p):
|
||||
x1, y1 = l1
|
||||
x2, y2 = l2
|
||||
x0, y0 = p
|
||||
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1))/point_distance(l1, l2)
|
||||
|
||||
def svg_arc(old, new, center, clockwise):
|
||||
r = point_distance(old, new)
|
||||
d = point_line_distance(old, new, center)
|
||||
sweep_flag = int(clockwise)
|
||||
large_arc = int((d > 0) == clockwise) # FIXME check signs
|
||||
return f'A {r:.6} {r:.6} {large_arc} {sweep_flag} {new[0]:.6} {new[1]:.6}'
|
||||
|
||||
@dataclass
|
||||
class ArcPoly(GraphicPrimitive):
|
||||
|
|
@ -72,15 +185,23 @@ class ArcPoly(GraphicPrimitive):
|
|||
outline : [(float,)]
|
||||
# list of radii of segments, must be either None (all segments are straight lines) or same length as outline.
|
||||
# Straight line segments have None entry.
|
||||
arc_centers : [(float,)]
|
||||
arc_centers : [(float,)] = None
|
||||
|
||||
@property
|
||||
def segments(self):
|
||||
return itertools.zip_longest(self.outline[:-1], self.outline[1:], self.radii or [])
|
||||
ol = self.outline
|
||||
return itertools.zip_longest(ol, ol[1:] + [ol[0]], self.arc_centers)
|
||||
|
||||
def bounds(self):
|
||||
for (x1, y1), (x2, y2), radius in self.segments:
|
||||
return
|
||||
def bounding_box(self):
|
||||
bbox = (None, None), (None, None)
|
||||
for (x1, y1), (x2, y2), arc in self.segments:
|
||||
if arc:
|
||||
clockwise, center = arc
|
||||
bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, *center, clockwise))
|
||||
|
||||
else:
|
||||
line_bounds = (min(x1, x2), min(y1, y2)), (max(x1, x2), max(y1, y2))
|
||||
bbox = add_bounds(bbox, line_bounds)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.outline)
|
||||
|
|
@ -88,6 +209,21 @@ class ArcPoly(GraphicPrimitive):
|
|||
def __bool__(self):
|
||||
return bool(len(self))
|
||||
|
||||
def _path_d(self):
|
||||
if len(self.outline) == 0:
|
||||
return
|
||||
|
||||
yield f'M {outline[0][0]:.6}, {outline[0][1]:.6}'
|
||||
for old, new, arc in self.segments:
|
||||
if not arc:
|
||||
yield f'L {new[0]:.6} {new[1]:.6}'
|
||||
else:
|
||||
clockwise, center = arc
|
||||
yield svg_arc(old, new, center, clockwise)
|
||||
|
||||
def to_svg(self):
|
||||
return 'path', [], {'d': ' '.join(self._path_d())}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Line(GraphicPrimitive):
|
||||
|
|
@ -97,7 +233,14 @@ class Line(GraphicPrimitive):
|
|||
y2 : float
|
||||
width : float
|
||||
|
||||
# FIXME bounds
|
||||
def bounding_box(self):
|
||||
r = self.width / 2
|
||||
return add_bounds(Circle(self.x1, self.y1, r).bounding_box(), Circle(self.x2, self.y2, r).bounding_box())
|
||||
|
||||
def to_svg(self):
|
||||
return 'path', [], dict(
|
||||
d=f'M {self.x1:.6} {self.y1:.6} L {self.x2:.6} {self.y2:.6}',
|
||||
style=f'stroke-width: {self.width:.6}; stroke-linecap: round')
|
||||
|
||||
@dataclass
|
||||
class Arc(GraphicPrimitive):
|
||||
|
|
@ -107,10 +250,36 @@ class Arc(GraphicPrimitive):
|
|||
y2 : float
|
||||
cx : float
|
||||
cy : float
|
||||
flipped : bool
|
||||
clockwise : bool
|
||||
width : float
|
||||
|
||||
# FIXME bounds
|
||||
def bounding_box(self):
|
||||
r = self.w/2
|
||||
endpoints = add_bounds(Circle(self.x1, self.y1, r).bounding_box(), Circle(self.x2, self.y2, r).bounding_box())
|
||||
|
||||
arc_r = point_distance((self.cx, self.cy), (self.x1, self.y1))
|
||||
|
||||
# extend C -> P1 line by line width / 2 along radius
|
||||
dx, dy = self.x1 - self.cx, self.y1 - self.cy
|
||||
x1 = self.x1 + dx/arc_r * r
|
||||
y1 = self.y1 + dy/arc_r * r
|
||||
|
||||
# same for C -> P2
|
||||
dx, dy = self.x2 - self.cx, self.y2 - self.cy
|
||||
x2 = self.x2 + dx/arc_r * r
|
||||
y2 = self.y2 + dy/arc_r * r
|
||||
|
||||
arc = arc_bounds(x1, y1, x2, y2, cx, cy, self.clockwise)
|
||||
return add_bounds(endpoints, arc) # FIXME add "include_center" switch
|
||||
|
||||
def to_svg(self):
|
||||
arc = svg_arc((self.x1, self.y1), (self.x2, self.y2), (self.cx, self.cy), self.clockwise)
|
||||
return 'path', [], dict(
|
||||
d=f'M {self.x1:.6} {self.y1:.6} {arc}',
|
||||
style=f'stroke-width: {self.width:.6}; stroke-linecap: round')
|
||||
|
||||
def svg_rotation(angle_rad):
|
||||
return f'rotation({angle_rad/math.pi*180:.4})'
|
||||
|
||||
@dataclass
|
||||
class Rectangle(GraphicPrimitive):
|
||||
|
|
@ -121,13 +290,29 @@ class Rectangle(GraphicPrimitive):
|
|||
h : float
|
||||
rotation : float # radians, around center!
|
||||
|
||||
def bounds(self):
|
||||
return ((self.x, self.y), (self.x+self.w, self.y+self.h))
|
||||
def bounding_box(self):
|
||||
return self.to_arc_poly().bounding_box()
|
||||
|
||||
def to_arc_poly(self):
|
||||
sin, cos = math.sin(self.rotation), math.cos(self.rotation)
|
||||
sw, cw = sin*self.w/2, cos*self.w/2
|
||||
sh, ch = sin*self.h/2, cos*self.h/2
|
||||
x, y = self.x, self.y
|
||||
return ArcPoly([
|
||||
(x - (cw+sh), y - (ch+sw)),
|
||||
(x - (cw+sh), y + (ch+sw)),
|
||||
(x + (cw+sh), y + (ch+sw)),
|
||||
(x + (cw+sh), y - (ch+sw)),
|
||||
])
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
return self.x + self.w/2, self.y + self.h/2
|
||||
|
||||
def to_svg(self):
|
||||
x, y = self.x - self.w/2, self.y - self.h/2
|
||||
return 'rect', [], dict(x=x, y=y, w=self.w, h=self.h, transform=svg_rotation(self.rotation))
|
||||
|
||||
|
||||
class RegularPolygon(GraphicPrimitive):
|
||||
x : float
|
||||
|
|
@ -136,13 +321,19 @@ class RegularPolygon(GraphicPrimitive):
|
|||
n : int
|
||||
rotation : float # radians!
|
||||
|
||||
def decompose(self):
|
||||
''' convert n-sided gerber polygon to normal Region defined by outline '''
|
||||
def to_arc_poly(self):
|
||||
''' convert n-sided gerber polygon to normal ArcPoly defined by outline '''
|
||||
|
||||
delta = 2*math.pi / self.n
|
||||
|
||||
yield Region([
|
||||
return ArcPoly([
|
||||
(self.x + math.cos(self.rotation + i*delta) * self.r,
|
||||
self.y + math.sin(self.rotation + i*delta) * self.r)
|
||||
for i in range(self.n) ])
|
||||
|
||||
def bounding_box(self):
|
||||
return self.to_arc_poly().bounding_box()
|
||||
|
||||
def to_svg(self):
|
||||
return self.to_arc_poly().to_svg()
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,18 @@ def points_close(a, b):
|
|||
else:
|
||||
return math.isclose(a[0], b[0]) and math.isclose(a[1], b[1])
|
||||
|
||||
def Tag:
|
||||
def __init__(self, name, children=None, **attrs):
|
||||
self.name, self.children, self.attrs = name, children, attrs
|
||||
|
||||
def __str__(self):
|
||||
opening = ' '.join([self.name] + [f'{key}="{value}"' for key, value in self.attrs.items()])
|
||||
if self.children:
|
||||
children = '\n'.join(textwrap.indent(str(c), ' ') for c in children)
|
||||
return f'<{opening}>\n{children}\n</{self.name}>'
|
||||
else:
|
||||
return f'<{opening}/>'
|
||||
|
||||
class GerberFile(CamFile):
|
||||
""" A class representing a single gerber file
|
||||
|
||||
|
|
@ -71,6 +83,27 @@ class GerberFile(CamFile):
|
|||
self.comments = []
|
||||
self.objects = []
|
||||
|
||||
def to_svg(self, tag=Tag, margin=0, margin_unit='mm', svg_unit='mm'):
|
||||
|
||||
(min_x, min_y), (max_x, max_y) = self.bounding_box(svg_unit)
|
||||
|
||||
if margin:
|
||||
margin = convert(margin, margin_unit, svg_unit)
|
||||
min_x -= margin
|
||||
min_y -= margin
|
||||
max_x += margin
|
||||
max_y += margin
|
||||
|
||||
w, h = max_x - min_x, max_y - min_y
|
||||
|
||||
primitives = [
|
||||
[ tag(*prim.to_svg()) for prim in obj.to_primitives(unit=svg_unit) ]
|
||||
for obj in self.objects ]
|
||||
|
||||
# FIXME setup viewport transform flipping y axis
|
||||
|
||||
return tag('svg', [defs, *primitives], width=w, height=h, viewBox=f'{min_x} {min_y} {w} {h}')
|
||||
|
||||
def merge(self, other):
|
||||
""" Merge other GerberFile into this one """
|
||||
self.comments += other.comments
|
||||
|
|
@ -158,8 +191,8 @@ class GerberFile(CamFile):
|
|||
return (x1 - x0, y1 - y0)
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
bounds = [ p.bounding_box for p in self.pDeprecatedrimitives ]
|
||||
def bounding_box(self, unit='mm'):
|
||||
bounds = [ p.bounding_box(unit) for p in self.objects ]
|
||||
|
||||
min_x = min(x0 for (x0, y0), (x1, y1) in bounds)
|
||||
min_y = min(y0 for (x0, y0), (x1, y1) in bounds)
|
||||
|
|
@ -227,27 +260,14 @@ class GerberFile(CamFile):
|
|||
def offset(self, dx=0, dy=0, unit='mm'):
|
||||
# TODO round offset to file resolution
|
||||
|
||||
dx, dy = self.convert_length(dx, unit), self.convert_length(dy, unit)
|
||||
#print(f'offset {dx},{dy} file unit')
|
||||
#for obj in self.objects:
|
||||
# print(' ', obj)
|
||||
self.objects = [ obj.with_offset(dx, dy) for obj in self.objects ]
|
||||
self.objects = [ obj.with_offset(dx, dy, unit) for obj in self.objects ]
|
||||
#print('after:')
|
||||
#for obj in self.objects:
|
||||
# print(' ', obj)
|
||||
|
||||
def convert_length(self, value, unit='mm'):
|
||||
""" Convert length into file unit """
|
||||
|
||||
if unit == 'mm':
|
||||
if self.unit == 'inch':
|
||||
return value / 25.4
|
||||
elif unit == 'inch':
|
||||
if self.unit == 'mm':
|
||||
return value * 25.4
|
||||
|
||||
return value
|
||||
|
||||
def rotate(self, angle:'radian', center=(0,0), unit='mm'):
|
||||
""" Rotate file contents around given point.
|
||||
|
||||
|
|
@ -261,8 +281,6 @@ class GerberFile(CamFile):
|
|||
if math.isclose(angle % (2*math.pi), 0):
|
||||
return
|
||||
|
||||
center = self.convert_length(center[0], unit), self.convert_length(center[1], unit)
|
||||
|
||||
# First, rotate apertures. We do this separately from rotating the individual objects below to rotate each
|
||||
# aperture exactly once.
|
||||
for ap in self.apertures:
|
||||
|
|
@ -273,7 +291,7 @@ class GerberFile(CamFile):
|
|||
# print(' ', obj)
|
||||
|
||||
for obj in self.objects:
|
||||
obj.rotate(angle, *center)
|
||||
obj.rotate(angle, *center, unit)
|
||||
|
||||
#print('after')
|
||||
#for obj in self.objects:
|
||||
|
|
@ -414,9 +432,9 @@ class GraphicsState:
|
|||
polarity_dark=self.polarity_dark, unit=self.file_settings.unit)
|
||||
|
||||
def _create_arc(self, old_point, new_point, control_point, aperture=True):
|
||||
direction = 'ccw' if self.interpolation_mode == CircularCCWModeStmt else 'cw'
|
||||
clockwise = self.interpolation_mode == CircularCWModeStmt
|
||||
return go.Arc(*old_point, *new_point,* self.map_coord(*control_point, relative=True),
|
||||
flipped=(direction == 'cw'), aperture=(self.aperture if aperture else None),
|
||||
clockwise=clockwise, aperture=(self.aperture if aperture else None),
|
||||
polarity_dark=self.polarity_dark, unit=self.file_settings.unit)
|
||||
|
||||
def update_point(self, x, y, unit=None):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue