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
|
import string
import math
from .sexp import *
from .base_types import *
from .primitives import *
from ... import graphic_objects as go
from ... import apertures as ap
from ...newstroke import Newstroke
from ...utils import rotate_point, MM
@sexp_type('layer')
class TextLayer:
layer: str = ''
knockout: Flag() = False
@sexp_type('gr_text')
class Text:
text: str = ''
at: AtPos = field(default_factory=AtPos)
layer: TextLayer = field(default_factory=TextLayer)
tstamp: Timestamp = None
effects: TextEffect = field(default_factory=TextEffect)
def render(self, variables={}):
if not self.effects or self.effects.hide or not self.effects.font:
return
font = Newstroke.load()
line_width = self.effects.font.thickness
text = string.Template(self.text).safe_substitute(variables)
strokes = list(font.render(text, size=self.effects.font.size.y))
min_x = min(x for st in strokes for x, y in st)
min_y = min(y for st in strokes for x, y in st)
max_x = max(x for st in strokes for x, y in st)
max_y = max(y for st in strokes for x, y in st)
w = max_x - min_x
h = max_y - min_y
offx = -min_x + {
None: -w/2,
Atom.right: -w,
Atom.left: 0
}[self.effects.justify.h if self.effects.justify else None]
offy = {
None: self.effects.font.size.y/2,
Atom.top: self.effects.font.size.y,
Atom.bottom: 0
}[self.effects.justify.v if self.effects.justify else None]
aperture = ap.CircleAperture(line_width or 0.2, unit=MM)
for stroke in strokes:
out = []
for x, y in stroke:
x, y = x+offx, y+offy
x, y = rotate_point(x, y, math.radians(self.at.rotation or 0))
x, y = x+self.at.x, y+self.at.y
out.append((x, y))
for p1, p2 in zip(out[:-1], out[1:]):
yield go.Line(*p1, *p2, aperture=aperture, unit=MM)
@sexp_type('gr_text_box')
class TextBox:
locked: Flag() = False
text: str = ''
start: Named(XYCoord) = None
end: Named(XYCoord) = None
pts: PointList = field(default_factory=PointList)
angle: OmitDefault(Named(float)) = 0.0
layer: Named(str) = ""
tstamp: Timestamp = None
effects: TextEffect = field(default_factory=TextEffect)
stroke: Stroke = field(default_factory=Stroke)
render_cache: RenderCache = None
def render(self, variables={}):
text = string.Template(self.text).safe_substitute(variables)
if text != self.text:
raise ValueError('Rendering of vector font text with variables not yet supported')
if not render_cache or not render_cache.polygons:
raise ValueError('Vector font text with empty render cache')
for poly in render_cache.polygons:
reg = go.Region([(p.x, p.y) for p in poly.pts.xy], unit=MM)
if self.stroke:
if self.stroke.type not in (None, Atom.default, Atom.solid):
raise ValueError('Dashed strokes are not supported on vector text')
yield from reg.outline_objects(aperture=ap.CircleAperture(self.stroke.width, unit=MM))
yield reg
@sexp_type('gr_line')
class Line:
start: Rename(XYCoord) = None
end: Rename(XYCoord) = None
angle: Named(float) = None # wat
layer: Named(str) = None
width: Named(float) = None
tstamp: Timestamp = None
def render(self, variables=None):
if self.angle:
raise NotImplementedError('Angles on lines are not implemented. Please raise an issue and provide an example file.')
aperture = ap.CircleAperture(self.width, unit=MM)
yield go.Line(self.start.x, self.start.y, self.end.x, self.end.y, aperture=aperture, unit=MM)
@sexp_type('fill')
class FillMode:
# Needed for compatibility with weird files
fill: AtomChoice(Atom.solid, Atom.yes, Atom.no, Atom.none) = False
@classmethod
def __map__(self, obj, parent=None):
return obj[1] in (Atom.solid, Atom.yes)
@classmethod
def __sexp__(self, value):
yield [Atom.fill, Atom.solid if value else Atom.none]
@sexp_type('gr_rect')
class Rectangle:
start: Rename(XYCoord) = None
end: Rename(XYCoord) = None
layer: Named(str) = None
width: Named(float) = None
fill: FillMode = False
tstamp: Timestamp = None
def render(self, variables=None):
rect = go.Region.from_rectangle(self.start.x, self.start.y,
self.end.x-self.start.x, self.end.y-self.start.y,
unit=MM)
if self.fill:
yield rect
if self.width:
yield from rect.outline_objects(aperture=ap.CircleAperture(self.width, unit=MM))
@sexp_type('gr_circle')
class Circle:
center: Rename(XYCoord) = None
end: Rename(XYCoord) = None
layer: Named(str) = None
width: Named(float) = None
fill: FillMode = False
tstamp: Timestamp = None
def render(self, variables=None):
r = math.dist((self.center.x, self.center.y), (self.end.x, self.end.y))
aperture = ap.CircleAperture(self.width or 0, unit=MM)
arc = go.Arc.from_circle(self.center.x, self.center.y, r, aperture=aperture, unit=MM)
if self.width:
yield arc
if self.fill:
yield arc.to_region()
@sexp_type('gr_arc')
class Arc:
start: Rename(XYCoord) = None
mid: Rename(XYCoord) = None
end: Rename(XYCoord) = None
layer: Named(str) = None
width: Named(float) = None
tstamp: Timestamp = None
def render(self, variables=None):
if not self.width:
return
cx, cy = self.mid.x, self.mid.y
x1, y1 = self.start.x, self.start.y
x2, y2 = self.end.x, self.end.y
yield go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, aperture=ap.CircleAperture(self.width or 0, unit=MM), clockwise=True, unit=MM)
@sexp_type('gr_poly')
class Polygon:
pts: PointList = field(default_factory=PointList)
layer: Named(str) = None
width: Named(float) = None
fill: FillMode = True
tstamp: Timestamp = None
def render(self, variables=None):
reg = go.Region([(pt.x, pt.y) for pt in self.pts.xy], unit=MM)
if self.width and self.width >= 0.005:
yield from reg.outline_objects(aperture=ap.CircleAperture(self.width, unit=MM))
if self.fill:
yield reg
@sexp_type('gr_curve')
class Curve:
pts: PointList = field(default_factory=PointList)
layer: Named(str) = None
width: Named(float) = None
tstamp: Timestamp = None
def render(self, variables=None):
raise NotImplementedError('Bezier rendering is not yet supported. Please raise an issue and provide an example file.')
@sexp_type('gr_bbox')
class AnnotationBBox:
start: Rename(XYCoord) = None
end: Rename(XYCoord) = None
def render(self, variables=None):
return []
|