1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
|
import math
import itertools
from dataclasses import dataclass, KW_ONLY, replace
from .utils import *
@dataclass
class GraphicPrimitive:
_ : KW_ONLY
polarity_dark : bool = True
def bounding_box(self):
""" Return the axis-aligned bounding box of this feature.
:returns: ``((min_x, min_Y), (max_x, max_y))``
:rtype: tuple
"""
raise NotImplementedError()
def to_svg(self, fg='black', bg='white', tag=Tag):
""" Render this primitive into its SVG representation.
:param str fg: Foreground color. Must be an SVG color name.
:param str bg: Background color. Must be an SVG color name.
:param function tag: Tag constructor to use.
:rtype: str
"""
raise NotImplementedError()
@dataclass
class Circle(GraphicPrimitive):
#: Center X coordinate
x : float
#: Center y coordinate
y : float
#: Radius, not diameter like in :py:class:`.apertures.CircleAperture`
r : float # Here, we use radius as common in modern computer graphics, not diameter as gerber uses.
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, fg='black', bg='white', tag=Tag):
color = fg if self.polarity_dark else bg
return tag('circle', cx=self.x, cy=self.y, r=self.r, style=f'fill: {color}')
@dataclass
class ArcPoly(GraphicPrimitive):
""" Polygon whose sides may be either straight lines or circular arcs. """
#: list of (x : float, y : float) tuples. Describes closed outline, i.e. the first and last point are considered
#: 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
@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``.
"""
ol = self.outline
return itertools.zip_longest(ol, ol[1:] + [ol[0]], self.arc_centers or [])
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:
line_bounds = (min(x1, x2), min(y1, y2)), (max(x1, x2), max(y1, y2))
bbox = add_bounds(bbox, line_bounds)
return bbox
@classmethod
def from_regular_polygon(kls, x:float, y:float, r:float, n:int, rotation:float=0, polarity_dark:bool=True):
""" Convert an n-sided gerber polygon to a normal ArcPoly defined by outline """
delta = 2*math.pi / self.n
return kls([
(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) ], polarity_dark=polarity_dark)
def __len__(self):
""" Return the number of points on this polygon's outline (which is also the number of segments because the
polygon is closed). """
return len(self.outline)
def __bool__(self):
""" Return ``True`` if this polygon has any outline points. """
return bool(len(self))
def _path_d(self):
if len(self.outline) == 0:
return
yield f'M {self.outline[0][0]:.6} {self.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, fg='black', bg='white', tag=Tag):
color = fg if self.polarity_dark else bg
return tag('path', d=' '.join(self._path_d()), style=f'fill: {color}')
@dataclass
class Line(GraphicPrimitive):
""" Straight line with round end caps. """
#: Start X coordinate. As usual in modern graphics APIs, this is at the center of the half-circle capping off this
#: line.
x1 : float
#: Start Y coordinate
y1 : float
#: End X coordinate
x2 : float
#: End Y coordinate
y2 : float
#: Line width
width : float
@classmethod
def from_obround(kls, x:float, y:float, w:float, h:float, rotation:float=0, polarity_dark:bool=True):
""" Convert a gerber obround into a :py:class:`~.graphic_primitives.Line`. """
if self.w > self.h:
w, a, b = self.h, self.w-self.h, 0
else:
w, a, b = self.w, 0, self.h-self.w
return kls(
*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, polarity_dark=self.polarity_dark)
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, fg='black', bg='white', tag=Tag):
color = fg if self.polarity_dark else bg
width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} L {self.x2:.6} {self.y2:.6}',
style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round')
@dataclass
class Arc(GraphicPrimitive):
""" Circular arc with line width ``width`` going from ``(x1, y1)`` to ``(x2, y2)`` around center at ``(cx, cy)``. """
#: Start X coodinate
x1 : float
#: Start Y coodinate
y1 : float
#: End X coodinate
x2 : float
#: End Y coodinate
y2 : float
#: Center X coordinate relative to ``x1``
cx : float
#: Center Y coordinate relative to ``y1``
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
clockwise : bool
#: Line width of this arc.
width : float
def bounding_box(self):
r = self.width/2
endpoints = add_bounds(Circle(self.x1, self.y1, r).bounding_box(), Circle(self.x2, self.y2, r).bounding_box())
arc_r = math.dist((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, self.cx, self.cy, self.clockwise)
return add_bounds(endpoints, arc) # FIXME add "include_center" switch
def to_svg(self, fg='black', bg='white', tag=Tag):
color = fg if self.polarity_dark else bg
arc = svg_arc((self.x1, self.y1), (self.x2, self.y2), (self.cx, self.cy), self.clockwise)
width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} {arc}',
style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round; fill: none')
@dataclass
class Rectangle(GraphicPrimitive):
#: **Center** X coordinate
x : float
#: **Center** Y coordinate
y : float
#: width
w : float
#: height
h : float
#: rotation around center in radians
rotation : float
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)),
])
def to_svg(self, fg='black', bg='white', tag=Tag):
color = fg if self.polarity_dark else bg
x, y = self.x - self.w/2, self.y - self.h/2
return tag('rect', x=x, y=y, width=self.w, height=self.h,
transform=svg_rotation(self.rotation, self.x, self.y), style=f'fill: {color}')
|