15 # You should have received a copy of the GNU General Public License |
13 # You should have received a copy of the GNU General Public License |
16 # along with Printrun. If not, see <http://www.gnu.org/licenses/>. |
14 # along with Printrun. If not, see <http://www.gnu.org/licenses/>. |
17 |
15 |
18 import wx |
16 import wx |
19 from math import log10, floor, ceil |
17 from math import log10, floor, ceil |
|
18 from bisect import bisect_left |
20 |
19 |
21 from printrun.utils import install_locale |
20 from printrun.utils import install_locale |
22 install_locale('pronterface') |
21 install_locale('pronterface') |
23 |
22 |
24 from .bufferedcanvas import BufferedCanvas |
23 from .bufferedcanvas import BufferedCanvas |
25 |
24 |
26 class GraphWindow(wx.Frame): |
25 class GraphWindow(wx.Frame): |
27 def __init__(self, root, parent_graph = None, size = (600, 600)): |
26 def __init__(self, root, parent_graph = None, size = (600, 600)): |
28 super(GraphWindow, self).__init__(None, title = _("Temperature graph"), |
27 super().__init__(None, title = _("Temperature graph"), |
29 size = size) |
28 size = size) |
30 panel = wx.Panel(self, -1) |
29 self.parentg = parent_graph |
|
30 panel = wx.Panel(self) |
31 vbox = wx.BoxSizer(wx.VERTICAL) |
31 vbox = wx.BoxSizer(wx.VERTICAL) |
32 self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph) |
32 self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph) |
33 vbox.Add(self.graph, 1, wx.EXPAND) |
33 vbox.Add(self.graph, 1, wx.EXPAND) |
34 panel.SetSizer(vbox) |
34 panel.SetSizer(vbox) |
35 |
35 |
|
36 def Destroy(self): |
|
37 self.graph.StopPlotting() |
|
38 if self.parentg is not None: |
|
39 self.parentg.window=None |
|
40 return super().Destroy() |
|
41 |
|
42 def __del__(self): |
|
43 if self.parentg is not None: |
|
44 self.parentg.window=None |
|
45 self.graph.StopPlotting() |
|
46 |
36 class Graph(BufferedCanvas): |
47 class Graph(BufferedCanvas): |
37 '''A class to show a Graph with Pronterface.''' |
48 '''A class to show a Graph with Pronterface.''' |
38 |
49 |
39 def __init__(self, parent, id, root, pos = wx.DefaultPosition, |
50 def __init__(self, parent, id, root, pos = wx.DefaultPosition, |
40 size = wx.Size(150, 80), style = 0, parent_graph = None): |
51 size = wx.Size(150, 80), style = 0, parent_graph = None): |
41 # Forcing a no full repaint to stop flickering |
52 # Forcing a no full repaint to stop flickering |
42 style = style | wx.NO_FULL_REPAINT_ON_RESIZE |
53 style = style | wx.NO_FULL_REPAINT_ON_RESIZE |
43 super(Graph, self).__init__(parent, id, pos, size, style) |
54 super().__init__(parent, id, pos, size, style) |
44 self.root = root |
55 self.root = root |
45 |
56 |
46 if parent_graph is not None: |
57 if parent_graph is not None: |
47 self.extruder0temps = parent_graph.extruder0temps |
58 self.extruder0temps = parent_graph.extruder0temps |
48 self.extruder0targettemps = parent_graph.extruder0targettemps |
59 self.extruder0targettemps = parent_graph.extruder0targettemps |
49 self.extruder1temps = parent_graph.extruder1temps |
60 self.extruder1temps = parent_graph.extruder1temps |
50 self.extruder1targettemps = parent_graph.extruder1targettemps |
61 self.extruder1targettemps = parent_graph.extruder1targettemps |
51 self.bedtemps = parent_graph.bedtemps |
62 self.bedtemps = parent_graph.bedtemps |
52 self.bedtargettemps = parent_graph.bedtargettemps |
63 self.bedtargettemps = parent_graph.bedtargettemps |
53 self.fanpowers=parent_graph.fanpowers |
64 self.fanpowers=parent_graph.fanpowers |
54 else: |
65 else: |
55 self.extruder0temps = [0] |
66 self.extruder0temps = [0] |
56 self.extruder0targettemps = [0] |
67 self.extruder0targettemps = [0] |
57 self.extruder1temps = [0] |
68 self.extruder1temps = [0] |
58 self.extruder1targettemps = [0] |
69 self.extruder1targettemps = [0] |
59 self.bedtemps = [0] |
70 self.bedtemps = [0] |
60 self.bedtargettemps = [0] |
71 self.bedtargettemps = [0] |
61 self.fanpowers= [0] |
72 self.fanpowers= [0] |
62 |
73 |
63 self.timer = wx.Timer(self) |
74 self.timer = wx.Timer(self) |
64 self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer) |
75 self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer) |
|
76 self.Bind(wx.EVT_WINDOW_DESTROY, self.processDestroy) |
65 |
77 |
66 self.minyvalue = 0 |
78 self.minyvalue = 0 |
67 self.maxyvalue = 260 |
79 self.maxyvalue = 260 |
68 self.rescaley = True # should the Y axis be rescaled dynamically? |
80 self.rescaley = True # should the Y axis be rescaled dynamically? |
69 if self.rescaley: |
81 if self.rescaley: |
88 |
107 |
89 def __del__(self): |
108 def __del__(self): |
90 if self.window: self.window.Close() |
109 if self.window: self.window.Close() |
91 |
110 |
92 def updateTemperatures(self, event): |
111 def updateTemperatures(self, event): |
|
112 # print('updateTemperatures') |
93 self.AddBedTemperature(self.bedtemps[-1]) |
113 self.AddBedTemperature(self.bedtemps[-1]) |
94 self.AddBedTargetTemperature(self.bedtargettemps[-1]) |
114 self.AddBedTargetTemperature(self.bedtargettemps[-1]) |
95 self.AddExtruder0Temperature(self.extruder0temps[-1]) |
115 self.AddExtruder0Temperature(self.extruder0temps[-1]) |
96 self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1]) |
116 self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1]) |
97 self.AddExtruder1Temperature(self.extruder1temps[-1]) |
117 self.AddExtruder1Temperature(self.extruder1temps[-1]) |
98 self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1]) |
118 self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1]) |
99 self.AddFanPower(self.fanpowers[-1]) |
119 self.AddFanPower(self.fanpowers[-1]) |
100 if self.rescaley: |
120 if self.rescaley: |
101 self._ybounds.update() |
121 self._ybounds.update() |
102 self.Refresh() |
122 self.Refresh() |
103 |
123 |
104 def drawgrid(self, dc, gc): |
124 def drawgrid(self, dc, gc): |
122 font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD) |
142 font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD) |
123 gc.SetFont(font, wx.Colour(23, 44, 44)) |
143 gc.SetFont(font, wx.Colour(23, 44, 44)) |
124 |
144 |
125 # draw vertical bars |
145 # draw vertical bars |
126 dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1)) |
146 dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1)) |
|
147 xscale = float(self.width - 1) / (self.xbars - 1) |
127 for x in range(self.xbars + 1): |
148 for x in range(self.xbars + 1): |
128 dc.DrawLine(x * (float(self.width - 1) / (self.xbars - 1)), |
149 x = x * xscale |
129 0, |
150 dc.DrawLine(x, 0, x, self.height) |
130 x * (float(self.width - 1) / (self.xbars - 1)), |
|
131 self.height) |
|
132 |
151 |
133 # draw horizontal bars |
152 # draw horizontal bars |
134 spacing = self._calculate_spacing() # spacing between bars, in degrees |
153 spacing = self._calculate_spacing() # spacing between bars, in degrees |
135 yspan = self.maxyvalue - self.minyvalue |
154 yspan = self.maxyvalue - self.minyvalue |
136 ybars = int(yspan / spacing) # Should be close to self.ybars |
155 ybars = int(yspan / spacing) # Should be close to self.ybars |
139 for y in range(firstbar, firstbar + ybars + 1): |
158 for y in range(firstbar, firstbar + ybars + 1): |
140 # y_pos = y*(float(self.height)/self.ybars) |
159 # y_pos = y*(float(self.height)/self.ybars) |
141 degrees = y * spacing |
160 degrees = y * spacing |
142 y_pos = self._y_pos(degrees) |
161 y_pos = self._y_pos(degrees) |
143 dc.DrawLine(0, y_pos, self.width, y_pos) |
162 dc.DrawLine(0, y_pos, self.width, y_pos) |
144 gc.DrawText(unicode(y * spacing), |
163 label = str(y * spacing) |
145 1, y_pos - (font.GetPointSize() / 2)) |
164 label_y = y_pos - font.GetPointSize() / 2 |
146 |
165 self.layoutText(label, 1, label_y, gc) |
147 if self.timer.IsRunning() is False: |
166 gc.DrawText(label, 1, label_y) |
|
167 |
|
168 if not self.timer.IsRunning(): |
148 font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD) |
169 font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD) |
149 gc.SetFont(font, wx.Colour(3, 4, 4)) |
170 gc.SetFont(font, wx.Colour(3, 4, 4)) |
150 gc.DrawText("Graph offline", |
171 gc.DrawText("Graph offline", |
151 self.width / 2 - (font.GetPointSize() * 3), |
172 self.width / 2 - font.GetPointSize() * 3, |
152 self.height / 2 - (font.GetPointSize() * 1)) |
173 self.height / 2 - font.GetPointSize() * 1) |
153 |
174 |
154 # dc.DrawCircle(50, 50, 1) |
175 # dc.DrawCircle(50, 50, 1) |
155 |
176 |
156 # gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1)) |
177 # gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1)) |
157 # gc.DrawLines([[20, 30], [10, 53]]) |
178 # gc.DrawLines([[20, 30], [10, 53]]) |
185 else: |
206 else: |
186 return 10 ** (exponent + 1) |
207 return 10 ** (exponent + 1) |
187 |
208 |
188 def drawtemperature(self, dc, gc, temperature_list, |
209 def drawtemperature(self, dc, gc, temperature_list, |
189 text, text_xoffset, r, g, b, a): |
210 text, text_xoffset, r, g, b, a): |
190 if self.timer.IsRunning() is False: |
211 color = self.timer.IsRunning() and (r, g, b, a) or [128] * 4 |
191 dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1)) |
212 dc.SetPen(wx.Pen(color, 1)) |
192 else: |
|
193 dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1)) |
|
194 |
213 |
195 x_add = float(self.width) / self.xsteps |
214 x_add = float(self.width) / self.xsteps |
196 x_pos = 0.0 |
215 x_pos = 0.0 |
197 lastxvalue = 0.0 |
216 lastxvalue = 0.0 |
198 lastyvalue = temperature_list[-1] |
217 lastyvalue = temperature_list[-1] |
199 |
218 |
200 for temperature in (temperature_list): |
219 for temperature in temperature_list: |
201 y_pos = self._y_pos(temperature) |
220 y_pos = self._y_pos(temperature) |
202 if (x_pos > 0.0): # One need 2 points to draw a line. |
221 if x_pos > 0: # One need 2 points to draw a line. |
203 dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos) |
222 dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos) |
204 |
223 |
205 lastxvalue = x_pos |
224 lastxvalue = x_pos |
206 x_pos = float(x_pos) + x_add |
225 x_pos += x_add |
207 lastyvalue = y_pos |
226 lastyvalue = y_pos |
208 |
227 |
209 if len(text) > 0: |
228 if text: |
210 font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD) |
229 font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD) |
211 # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL) |
230 # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL) |
212 if self.timer.IsRunning() is False: |
231 gc.SetFont(font, color[:3]) |
213 gc.SetFont(font, wx.Colour(128, 128, 128)) |
|
214 else: |
|
215 gc.SetFont(font, wx.Colour(r, g, b)) |
|
216 |
232 |
217 text_size = len(text) * text_xoffset + 1 |
233 text_size = len(text) * text_xoffset + 1 |
218 gc.DrawText(text, |
234 pos = self.layoutText(text, lastxvalue, lastyvalue, gc) |
219 x_pos - x_add - (font.GetPointSize() * text_size), |
235 gc.DrawText(text, pos.x, pos.y) |
220 lastyvalue - (font.GetPointSize() / 2)) |
236 |
|
237 def layoutRect(self, rc): |
|
238 res = LtRect(rc) |
|
239 reserved = sorted((rs for rs in self.reserved |
|
240 if not (rc.bottom < rs.top or rc.top > rs.bottom)), |
|
241 key=wx.Rect.GetLeft) |
|
242 self.boundRect(res) |
|
243 # search to the left for gaps large enough to accomodate res |
|
244 rci = bisect_left(reserved, res) |
|
245 |
|
246 for i in range(rci, len(reserved)-1): |
|
247 res.x = reserved[i].right + 1 |
|
248 if res.right < reserved[i+1].left: |
|
249 #found good res |
|
250 break |
|
251 else: |
|
252 # did not find gap to the right |
|
253 if reserved: |
|
254 #try to respect rc.x at the cost of a gap (50...Bed) |
|
255 if res.left < reserved[-1].right: |
|
256 res.x = reserved[-1].right + 1 |
|
257 if res.right >= self.width: |
|
258 #goes beyond window bounds |
|
259 # try to the left |
|
260 for i in range(min(rci, len(reserved)-1), 0, -1): |
|
261 res.x = reserved[i].left - rc.width |
|
262 if reserved[i-1].right < res.left: |
|
263 break |
|
264 else: |
|
265 res = LtRect(self.layoutRectY(rc)) |
|
266 |
|
267 self.reserved.append(res) |
|
268 return res |
|
269 |
|
270 def boundRect(self, rc): |
|
271 rc.x = min(rc.x, self.width - rc.width) |
|
272 return rc |
|
273 |
|
274 def layoutRectY(self, rc): |
|
275 top = self.height |
|
276 bottom = 0 |
|
277 collision = False |
|
278 res = LtRect(rc) |
|
279 res.x = max(self.gridLabelsRight+1, min(rc.x, self.width-rc.width)) |
|
280 for rs in self.reserved: |
|
281 if not (res.right < rs.left or res.left > rs.right): |
|
282 collision = True |
|
283 top = min(top, rs.Top) |
|
284 bottom = max(bottom, rs.bottom) |
|
285 if collision: |
|
286 res.y = top - rc.height |
|
287 if res.y < 0: |
|
288 res.y = bottom+1 |
|
289 if res.bottom >= self.height: |
|
290 res.y = rc.y |
|
291 return res |
|
292 |
|
293 def layoutText(self, text, x, y, gc): |
|
294 ext = gc.GetTextExtent(text) |
|
295 rc = self.layoutRect(wx.Rect(x, y, *ext)) |
|
296 # print('layoutText', text, rc.TopLeft) |
|
297 return rc |
221 |
298 |
222 def drawfanpower(self, dc, gc): |
299 def drawfanpower(self, dc, gc): |
223 self.drawtemperature(dc, gc, self.fanpowers, |
300 self.drawtemperature(dc, gc, self.fanpowers, |
224 "Fan", 1, 0, 0, 0, 128) |
301 "Fan", 1, 0, 0, 0, 128) |
225 |
302 |
313 def StartPlotting(self, time): |
390 def StartPlotting(self, time): |
314 self.Refresh() |
391 self.Refresh() |
315 self.timer.Start(time) |
392 self.timer.Start(time) |
316 if self.window: self.window.graph.StartPlotting(time) |
393 if self.window: self.window.graph.StartPlotting(time) |
317 |
394 |
|
395 def Destroy(self): |
|
396 # print(__class__, '.Destroy') |
|
397 self.StopPlotting() |
|
398 return super(BufferedCanvas, self).Destroy() |
|
399 |
318 def StopPlotting(self): |
400 def StopPlotting(self): |
319 self.timer.Stop() |
401 self.timer.Stop() |
320 self.Refresh() |
402 #self.Refresh() # do not refresh when stopping in case the underlying object has been destroyed already |
321 if self.window: self.window.graph.StopPlotting() |
403 if self.window: self.window.graph.StopPlotting() |
322 |
404 |
323 def draw(self, dc, w, h): |
405 def draw(self, dc, w, h): |
324 dc.SetBackground(wx.Brush(self.root.bgcolor)) |
406 dc.SetBackground(wx.Brush(self.root.settings.graph_color_background)) |
325 dc.Clear() |
407 dc.Clear() |
326 gc = wx.GraphicsContext.Create(dc) |
408 gc = wx.GraphicsContext.Create(dc) |
327 self.width = w |
409 self.width = w |
328 self.height = h |
410 self.height = h |
|
411 |
|
412 self.reserved.clear() |
329 self.drawgrid(dc, gc) |
413 self.drawgrid(dc, gc) |
|
414 self.gridLabelsRight = self.reserved[-1].Right |
|
415 |
330 self.drawbedtargettemp(dc, gc) |
416 self.drawbedtargettemp(dc, gc) |
331 self.drawbedtemp(dc, gc) |
417 self.drawbedtemp(dc, gc) |
332 self.drawfanpower(dc, gc) |
418 self.drawfanpower(dc, gc) |
333 self.drawextruder0targettemp(dc, gc) |
419 self.drawextruder0targettemp(dc, gc) |
334 self.drawextruder0temp(dc, gc) |
420 self.drawextruder0temp(dc, gc) |
335 self.drawextruder1targettemp(dc, gc) |
421 if self.extruder1targettemps[-1]>0 or self.extruder1temps[-1]>5: |
336 self.drawextruder1temp(dc, gc) |
422 self.drawextruder1targettemp(dc, gc) |
337 |
423 self.drawextruder1temp(dc, gc) |
338 class _YBounds(object): |
424 |
|
425 class _YBounds: |
339 """Small helper class to claculate y bounds dynamically""" |
426 """Small helper class to claculate y bounds dynamically""" |
340 |
427 |
341 def __init__(self, graph, minimum_scale=5.0, buffer=0.10): |
428 def __init__(self, graph, minimum_scale=5.0, buffer=0.10): |
342 """_YBounds(Graph,float,float) |
429 """_YBounds(Graph,float,float) |
343 |
430 |
434 miny = min(miny, extruder1_min, extruder1_target) |
521 miny = min(miny, extruder1_min, extruder1_target) |
435 maxy = max(maxy, extruder1_max, extruder1_target) |
522 maxy = max(maxy, extruder1_max, extruder1_target) |
436 if bed_target > 0 or bed_max > 5: # use HBP |
523 if bed_target > 0 or bed_max > 5: # use HBP |
437 miny = min(miny, bed_min, bed_target) |
524 miny = min(miny, bed_min, bed_target) |
438 maxy = max(maxy, bed_max, bed_target) |
525 maxy = max(maxy, bed_max, bed_target) |
439 miny=min(0,miny); |
526 miny = min(0, miny) |
440 maxy=max(260,maxy); |
527 maxy = max(260, maxy) |
441 |
528 |
442 # We have to rescale, so add padding |
529 # We have to rescale, so add padding |
443 bufratio = self.buffer / (1.0 - self.buffer) |
530 bufratio = self.buffer / (1.0 - self.buffer) |
444 if miny < self.graph.minyvalue: |
531 if miny < self.graph.minyvalue: |
445 padding = (self.graph.maxyvalue - miny) * bufratio |
532 padding = (self.graph.maxyvalue - miny) * bufratio |