1
2import math
3
4# TODO: Support drawing ellipses without manually using Path
5
6
7class DrawingElement:
8 ''' Base class for drawing elements
9
10 Subclasses must implement writeSvgElement '''
11 def writeSvgElement(self, outputFile):
12 raise NotImplementedError('Abstract base class')
13 def __eq__(self, other):
14 return self is other
15
16class DrawingBasicElement(DrawingElement):
17 ''' Base class for SVG drawing elements that are a single node with no
18 child nodes '''
19 TAG_NAME = '_'
20 def __init__(self, **args):
21 self.args = args
22 def writeSvgElement(self, outputFile):
23 outputFile.write('<')
24 outputFile.write(self.TAG_NAME)
25 outputFile.write(' ')
26 for k, v in self.args.items():
27 k = k.replace('_', '-')
28 outputFile.write('{}="{}" '.format(k,v))
29 outputFile.write('/>')
30 def __eq__(self, other):
31 if isinstance(other, type(self)):
32 return (self.tagName == other.tagName and
33 self.args == other.args)
34 return False
35
36class NoElement(DrawingElement):
37 ''' A drawing element that has no effect '''
38 def __init__(self): pass
39 def writeSvgElement(self, outputFile):
40 pass
41 def __eq__(self, other):
42 if isinstance(other, type(self)):
43 return True
44 return False
45
46class Rectangle(DrawingBasicElement):
47 ''' A rectangle
48
49 Additional keyword arguments are ouput as additional arguments to the
50 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
51 TAG_NAME = 'rect'
52 def __init__(self, x, y, width, height, **kwargs):
53 super().__init__(x=x, y=-y-height, width=width, height=height,
54 **kwargs)
55
56class Circle(DrawingBasicElement):
57 ''' A circle
58
59 Additional keyword arguments are ouput as additional properties to the
60 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
61 TAG_NAME = 'circle'
62 def __init__(self, cx, cy, r, **kwargs):
63 super().__init__(cx=cx, cy=-cy, r=r, **kwargs)
64
65class ArcLine(Circle):
66 ''' An arc
67
68 In most cases, use Arc instead of ArcLine. ArcLine uses the
69 stroke-dasharray SVG property to make the edge of a circle look like
70 an arc.
71
72 Additional keyword arguments are ouput as additional arguments to the
73 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
74 def __init__(self, cx, cy, r, startDeg, endDeg, **kwargs):
75 if endDeg - startDeg == 360:
76 super().__init__(cx, cy, r, **kwargs)
77 return
78 startDeg, endDeg = (-endDeg) % 360, (-startDeg) % 360
79 arcDeg = (endDeg - startDeg) % 360
80 def arcLen(deg): return math.radians(deg) * r
81 wholeLen = 2 * math.pi * r
82 if endDeg == startDeg:
83 offset = 1
84 dashes = "0 {}".format(wholeLen+2)
85 #elif endDeg >= startDeg:
86 elif True:
87 startLen = arcLen(startDeg)
88 arcLen = arcLen(arcDeg)
89 offLen = wholeLen - arcLen
90 offset = -startLen
91 dashes = "{} {}".format(arcLen, offLen)
92 #else:
93 # firstLen = arcLen(endDeg)
94 # secondLen = arcLen(360-startDeg)
95 # gapLen = wholeLen - firstLen - secondLen
96 # offset = 0
97 # dashes = "{} {} {}".format(firstLen, gapLen, secondLen)
98 super().__init__(cx, cy, r, stroke_dasharray=dashes,
99 stroke_dashoffset=offset, **kwargs)
100
101class Path(DrawingBasicElement):
102 ''' An arbitrary path
103
104 Path Supports building an SVG path by calling instance methods
105 corresponding to path commands.
106
107 Additional keyword arguments are ouput as additional properties to the
108 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
109 TAG_NAME = 'path'
110 def __init__(self, d='', **kwargs):
111 super().__init__(d=d, **kwargs)
112 def append(self, commandStr, *args):
113 if len(self.args['d']) > 0:
114 commandStr = ' ' + commandStr
115 if len(args) > 0:
116 commandStr = commandStr + ','.join(map(str, args))
117 self.args['d'] += commandStr
118 def M(self, x, y): self.append('M', x, -y)
119 def m(self, dx, dy): self.append('m', dx, -dy)
120 def L(self, x, y): self.append('L', x, -y)
121 def l(self, dx, dy): self.append('l', dx, -dy)
122 def H(self, x, y): self.append('H', x)
123 def h(self, dx): self.append('h', dx)
124 def V(self, y): self.append('V', -y)
125 def v(self, dy): self.append('v', -dy)
126 def Z(self): self.append('Z')
127 def C(self, cx1, cy1, cx2, cy2, ex, ey):
128 self.append('C', cx1, -cy1, cx2, -cy2, ex, -ey)
129 def c(self, cx1, cy1, cx2, cy2, ex, ey):
130 self.append('c', cx1, -cy1, cx2, -cy2, ex, -ey)
131 def S(self, cx2, cy2, ex, ey): self.append('S', cx2, -cy2, ex, -ey)
132 def s(self, cx2, cy2, ex, ey): self.append('s', cx2, -cy2, ex, -ey)
133 def Q(self, cx, cy, ex, ey): self.append('Q', cx, -cy, ex, -ey)
134 def q(self, cx, cy, ex, ey): self.append('q', cx, -cy, ex, -ey)
135 def T(self, ex, ey): self.append('T', ex, -ey)
136 def t(self, ex, ey): self.append('t', ex, -ey)
137 def A(self, rx, ry, rot, largeArc, sweep, ex, ey):
138 self.append('A', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey)
139 def a(self, rx, ry, rot, largeArc, sweep, ex, ey):
140 self.append('a', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey)
141 def arc(self, cx, cy, r, startDeg, endDeg, cw=False, includeM=True, includeL=False):
142 ''' Uses A() to draw a circular arc '''
143 largeArc = (endDeg - startDeg) % 360 > 180
144 startRad, endRad = startDeg*math.pi/180, endDeg*math.pi/180
145 sx, sy = r*math.cos(startRad), r*math.sin(startRad)
146 ex, ey = r*math.cos(endRad), r*math.sin(endRad)
147 if includeL:
148 self.L(cx+sx, cy+sy)
149 elif includeM:
150 self.M(cx+sx, cy+sy)
151 self.A(r, r, 0, largeArc ^ cw, cw, cx+ex, cy+ey)
152
153class Lines(Path):
154 ''' A sequence of connected lines (or a polygon)
155
156 Additional keyword arguments are ouput as additional properties to the
157 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
158 def __init__(self, sx, sy, *points, close=False, **kwargs):
159 super().__init__(d='', **kwargs)
160 self.M(sx, sy)
161 assert len(points) % 2 == 0
162 for i in range(len(points) // 2):
163 self.L(points[2*i], points[2*i+1])
164 if close:
165 self.Z()
166
167class Line(Lines):
168 ''' A line
169
170 Additional keyword arguments are ouput as additional properties to the
171 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
172 def __init__(self, sx, sy, ex, ey, **kwargs):
173 super().__init__(sx, sy, ex, ey, close=False, **kwargs)
174
175class Arc(Path):
176 ''' An arc
177
178 Additional keyword arguments are ouput as additional properties to the
179 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
180 def __init__(self, cx, cy, r, startDeg, endDeg, cw=False, **kwargs):
181 super().__init__(d='', **kwargs)
182 self.arc(cx, cy, r, startDeg, endDeg, cw=cw, includeM=True)
183