|
1 #!/usr/bin/env python |
|
2 |
|
3 # This file is part of the Printrun suite. |
|
4 # |
|
5 # Printrun is free software: you can redistribute it and/or modify |
|
6 # it under the terms of the GNU General Public License as published by |
|
7 # the Free Software Foundation, either version 3 of the License, or |
|
8 # (at your option) any later version. |
|
9 # |
|
10 # Printrun is distributed in the hope that it will be useful, |
|
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 # GNU General Public License for more details. |
|
14 # |
|
15 # You should have received a copy of the GNU General Public License |
|
16 # along with Printrun. If not, see <http://www.gnu.org/licenses/>. |
|
17 |
|
18 import logging |
|
19 import wx |
|
20 |
|
21 from . import gcoder |
|
22 from .gl.panel import wxGLPanel |
|
23 from .gl.trackball import build_rotmatrix |
|
24 from .gl.libtatlin import actors |
|
25 from .injectgcode import injector, injector_edit |
|
26 |
|
27 from pyglet.gl import glPushMatrix, glPopMatrix, \ |
|
28 glTranslatef, glRotatef, glScalef, glMultMatrixd, \ |
|
29 glGetDoublev, GL_MODELVIEW_MATRIX, GLdouble |
|
30 |
|
31 from .gviz import GvizBaseFrame |
|
32 |
|
33 from .utils import imagefile, install_locale, get_home_pos |
|
34 install_locale('pronterface') |
|
35 |
|
36 def create_model(light): |
|
37 if light: |
|
38 return actors.GcodeModelLight() |
|
39 else: |
|
40 return actors.GcodeModel() |
|
41 |
|
42 def gcode_dims(g): |
|
43 return ((g.xmin, g.xmax, g.width), |
|
44 (g.ymin, g.ymax, g.depth), |
|
45 (g.zmin, g.zmax, g.height)) |
|
46 |
|
47 def set_model_colors(model, root): |
|
48 for field in dir(model): |
|
49 if field.startswith("color_"): |
|
50 root_fieldname = "gcview_" + field |
|
51 if hasattr(root, root_fieldname): |
|
52 setattr(model, field, getattr(root, root_fieldname)) |
|
53 |
|
54 def recreate_platform(self, build_dimensions, circular): |
|
55 self.platform = actors.Platform(build_dimensions, circular = circular) |
|
56 self.objects[0].model = self.platform |
|
57 wx.CallAfter(self.Refresh) |
|
58 |
|
59 def set_gcview_params(self, path_width, path_height): |
|
60 self.path_halfwidth = path_width / 2 |
|
61 self.path_halfheight = path_height / 2 |
|
62 has_changed = False |
|
63 for obj in self.objects[1:]: |
|
64 if isinstance(obj.model, actors.GcodeModel): |
|
65 obj.model.set_path_size(self.path_halfwidth, self.path_halfheight) |
|
66 has_changed = True |
|
67 return has_changed |
|
68 |
|
69 class GcodeViewPanel(wxGLPanel): |
|
70 |
|
71 def __init__(self, parent, id = wx.ID_ANY, |
|
72 build_dimensions = None, realparent = None, |
|
73 antialias_samples = 0): |
|
74 super(GcodeViewPanel, self).__init__(parent, id, wx.DefaultPosition, |
|
75 wx.DefaultSize, 0, |
|
76 antialias_samples = antialias_samples) |
|
77 self.canvas.Bind(wx.EVT_MOUSE_EVENTS, self.move) |
|
78 self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.double) |
|
79 self.canvas.Bind(wx.EVT_KEY_DOWN, self.keypress) |
|
80 self.initialized = 0 |
|
81 self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.wheel) |
|
82 self.parent = realparent if realparent else parent |
|
83 self.initpos = None |
|
84 if build_dimensions: |
|
85 self.build_dimensions = build_dimensions |
|
86 else: |
|
87 self.build_dimensions = [200, 200, 100, 0, 0, 0] |
|
88 self.dist = max(self.build_dimensions[0], self.build_dimensions[1]) |
|
89 self.basequat = [0, 0, 0, 1] |
|
90 self.mousepos = [0, 0] |
|
91 |
|
92 def inject(self): |
|
93 l = self.parent.model.num_layers_to_draw |
|
94 filtered = [k for k, v in self.parent.model.layer_idxs_map.iteritems() if v == l] |
|
95 if filtered: |
|
96 injector(self.parent.model.gcode, l, filtered[0]) |
|
97 else: |
|
98 logging.error(_("Invalid layer for injection")) |
|
99 |
|
100 def editlayer(self): |
|
101 l = self.parent.model.num_layers_to_draw |
|
102 filtered = [k for k, v in self.parent.model.layer_idxs_map.iteritems() if v == l] |
|
103 if filtered: |
|
104 injector_edit(self.parent.model.gcode, l, filtered[0]) |
|
105 else: |
|
106 logging.error(_("Invalid layer for edition")) |
|
107 |
|
108 def setlayercb(self, layer): |
|
109 pass |
|
110 |
|
111 def OnInitGL(self, *args, **kwargs): |
|
112 super(GcodeViewPanel, self).OnInitGL(*args, **kwargs) |
|
113 if hasattr(self.parent, "filenames") and self.parent.filenames: |
|
114 for filename in self.parent.filenames: |
|
115 self.parent.load_file(filename) |
|
116 self.parent.autoplate() |
|
117 if hasattr(self.parent, "loadcb"): |
|
118 self.parent.loadcb() |
|
119 self.parent.filenames = None |
|
120 |
|
121 def create_objects(self): |
|
122 '''create opengl objects when opengl is initialized''' |
|
123 for obj in self.parent.objects: |
|
124 if obj.model and obj.model.loaded and not obj.model.initialized: |
|
125 obj.model.init() |
|
126 |
|
127 def update_object_resize(self): |
|
128 '''called when the window recieves only if opengl is initialized''' |
|
129 pass |
|
130 |
|
131 def draw_objects(self): |
|
132 '''called in the middle of ondraw after the buffer has been cleared''' |
|
133 self.create_objects() |
|
134 |
|
135 glPushMatrix() |
|
136 # Rotate according to trackball |
|
137 glMultMatrixd(build_rotmatrix(self.basequat)) |
|
138 # Move origin to bottom left of platform |
|
139 platformx0 = -self.build_dimensions[3] - self.parent.platform.width / 2 |
|
140 platformy0 = -self.build_dimensions[4] - self.parent.platform.depth / 2 |
|
141 glTranslatef(platformx0, platformy0, 0) |
|
142 |
|
143 for obj in self.parent.objects: |
|
144 if not obj.model \ |
|
145 or not obj.model.loaded \ |
|
146 or not obj.model.initialized: |
|
147 continue |
|
148 glPushMatrix() |
|
149 glTranslatef(*(obj.offsets)) |
|
150 glRotatef(obj.rot, 0.0, 0.0, 1.0) |
|
151 glTranslatef(*(obj.centeroffset)) |
|
152 glScalef(*obj.scale) |
|
153 |
|
154 obj.model.display() |
|
155 glPopMatrix() |
|
156 glPopMatrix() |
|
157 |
|
158 # ========================================================================== |
|
159 # Utils |
|
160 # ========================================================================== |
|
161 def get_modelview_mat(self, local_transform): |
|
162 mvmat = (GLdouble * 16)() |
|
163 if local_transform: |
|
164 glPushMatrix() |
|
165 # Rotate according to trackball |
|
166 glMultMatrixd(build_rotmatrix(self.basequat)) |
|
167 # Move origin to bottom left of platform |
|
168 platformx0 = -self.build_dimensions[3] - self.parent.platform.width / 2 |
|
169 platformy0 = -self.build_dimensions[4] - self.parent.platform.depth / 2 |
|
170 glTranslatef(platformx0, platformy0, 0) |
|
171 glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) |
|
172 glPopMatrix() |
|
173 else: |
|
174 glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) |
|
175 return mvmat |
|
176 |
|
177 def double(self, event): |
|
178 if hasattr(self.parent, "clickcb") and self.parent.clickcb: |
|
179 self.parent.clickcb(event) |
|
180 |
|
181 def move(self, event): |
|
182 """react to mouse actions: |
|
183 no mouse: show red mousedrop |
|
184 LMB: rotate viewport |
|
185 RMB: move viewport |
|
186 """ |
|
187 if event.Entering(): |
|
188 self.canvas.SetFocus() |
|
189 event.Skip() |
|
190 return |
|
191 if event.Dragging() and event.LeftIsDown(): |
|
192 self.handle_rotation(event) |
|
193 elif event.Dragging() and event.RightIsDown(): |
|
194 self.handle_translation(event) |
|
195 elif event.LeftUp(): |
|
196 self.initpos = None |
|
197 elif event.RightUp(): |
|
198 self.initpos = None |
|
199 else: |
|
200 event.Skip() |
|
201 return |
|
202 event.Skip() |
|
203 wx.CallAfter(self.Refresh) |
|
204 |
|
205 def layerup(self): |
|
206 if not hasattr(self.parent, "model") or not self.parent.model: |
|
207 return |
|
208 max_layers = self.parent.model.max_layers |
|
209 current_layer = self.parent.model.num_layers_to_draw |
|
210 # accept going up to max_layers + 1 |
|
211 # max_layers means visualizing the last layer differently, |
|
212 # max_layers + 1 means visualizing all layers with the same color |
|
213 new_layer = min(max_layers + 1, current_layer + 1) |
|
214 self.parent.model.num_layers_to_draw = new_layer |
|
215 self.parent.setlayercb(new_layer) |
|
216 wx.CallAfter(self.Refresh) |
|
217 |
|
218 def layerdown(self): |
|
219 if not hasattr(self.parent, "model") or not self.parent.model: |
|
220 return |
|
221 current_layer = self.parent.model.num_layers_to_draw |
|
222 new_layer = max(1, current_layer - 1) |
|
223 self.parent.model.num_layers_to_draw = new_layer |
|
224 self.parent.setlayercb(new_layer) |
|
225 wx.CallAfter(self.Refresh) |
|
226 |
|
227 def handle_wheel(self, event): |
|
228 delta = event.GetWheelRotation() |
|
229 factor = 1.05 |
|
230 if event.ControlDown(): |
|
231 factor = 1.02 |
|
232 if hasattr(self.parent, "model") and event.ShiftDown(): |
|
233 if not self.parent.model: |
|
234 return |
|
235 count = 1 if not event.ControlDown() else 10 |
|
236 for i in range(count): |
|
237 if delta > 0: self.layerup() |
|
238 else: self.layerdown() |
|
239 return |
|
240 x, y = event.GetPositionTuple() |
|
241 x, y, _ = self.mouse_to_3d(x, y) |
|
242 if delta > 0: |
|
243 self.zoom(factor, (x, y)) |
|
244 else: |
|
245 self.zoom(1 / factor, (x, y)) |
|
246 |
|
247 def wheel(self, event): |
|
248 """react to mouse wheel actions: |
|
249 without shift: set max layer |
|
250 with shift: zoom viewport |
|
251 """ |
|
252 self.handle_wheel(event) |
|
253 wx.CallAfter(self.Refresh) |
|
254 |
|
255 def fit(self): |
|
256 if not self.parent.model or not self.parent.model.loaded: |
|
257 return |
|
258 self.canvas.SetCurrent(self.context) |
|
259 dims = gcode_dims(self.parent.model.gcode) |
|
260 self.reset_mview(1.0) |
|
261 center_x = (dims[0][0] + dims[0][1]) / 2 |
|
262 center_y = (dims[1][0] + dims[1][1]) / 2 |
|
263 center_x = self.build_dimensions[0] / 2 - center_x |
|
264 center_y = self.build_dimensions[1] / 2 - center_y |
|
265 if self.orthographic: |
|
266 ratio = float(self.dist) / max(dims[0][2], dims[1][2]) |
|
267 glScalef(ratio, ratio, 1) |
|
268 glTranslatef(center_x, center_y, 0) |
|
269 wx.CallAfter(self.Refresh) |
|
270 |
|
271 def keypress(self, event): |
|
272 """gets keypress events and moves/rotates acive shape""" |
|
273 step = 1.1 |
|
274 if event.ControlDown(): |
|
275 step = 1.05 |
|
276 kup = [85, 315] # Up keys |
|
277 kdo = [68, 317] # Down Keys |
|
278 kzi = [wx.WXK_PAGEDOWN, 388, 316, 61] # Zoom In Keys |
|
279 kzo = [wx.WXK_PAGEUP, 390, 314, 45] # Zoom Out Keys |
|
280 kfit = [70] # Fit to print keys |
|
281 kshowcurrent = [67] # Show only current layer keys |
|
282 kreset = [82] # Reset keys |
|
283 key = event.GetKeyCode() |
|
284 if key in kup: |
|
285 self.layerup() |
|
286 if key in kdo: |
|
287 self.layerdown() |
|
288 x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) |
|
289 if key in kzi: |
|
290 self.zoom_to_center(step) |
|
291 if key in kzo: |
|
292 self.zoom_to_center(1 / step) |
|
293 if key in kfit: |
|
294 self.fit() |
|
295 if key in kshowcurrent: |
|
296 if not self.parent.model or not self.parent.model.loaded: |
|
297 return |
|
298 self.parent.model.only_current = not self.parent.model.only_current |
|
299 wx.CallAfter(self.Refresh) |
|
300 if key in kreset: |
|
301 self.resetview() |
|
302 event.Skip() |
|
303 |
|
304 def resetview(self): |
|
305 self.canvas.SetCurrent(self.context) |
|
306 self.reset_mview(0.9) |
|
307 self.basequat = [0, 0, 0, 1] |
|
308 wx.CallAfter(self.Refresh) |
|
309 |
|
310 class GCObject(object): |
|
311 |
|
312 def __init__(self, model): |
|
313 self.offsets = [0, 0, 0] |
|
314 self.centeroffset = [0, 0, 0] |
|
315 self.rot = 0 |
|
316 self.curlayer = 0.0 |
|
317 self.scale = [1.0, 1.0, 1.0] |
|
318 self.model = model |
|
319 |
|
320 class GcodeViewLoader(object): |
|
321 |
|
322 path_halfwidth = 0.2 |
|
323 path_halfheight = 0.15 |
|
324 |
|
325 def addfile_perlayer(self, gcode = None, showall = False): |
|
326 self.model = create_model(self.root.settings.light3d |
|
327 if self.root else False) |
|
328 if isinstance(self.model, actors.GcodeModel): |
|
329 self.model.set_path_size(self.path_halfwidth, self.path_halfheight) |
|
330 self.objects[-1].model = self.model |
|
331 if self.root: |
|
332 set_model_colors(self.model, self.root) |
|
333 if gcode is not None: |
|
334 generator = self.model.load_data(gcode) |
|
335 generator_output = generator.next() |
|
336 while generator_output is not None: |
|
337 yield generator_output |
|
338 generator_output = generator.next() |
|
339 wx.CallAfter(self.Refresh) |
|
340 yield None |
|
341 |
|
342 def addfile(self, gcode = None, showall = False): |
|
343 generator = self.addfile_perlayer(gcode, showall) |
|
344 while generator.next() is not None: |
|
345 continue |
|
346 |
|
347 def set_gcview_params(self, path_width, path_height): |
|
348 return set_gcview_params(self, path_width, path_height) |
|
349 |
|
350 class GcodeViewMainWrapper(GcodeViewLoader): |
|
351 |
|
352 def __init__(self, parent, build_dimensions, root, circular, antialias_samples): |
|
353 self.root = root |
|
354 self.glpanel = GcodeViewPanel(parent, realparent = self, |
|
355 build_dimensions = build_dimensions, |
|
356 antialias_samples = antialias_samples) |
|
357 self.glpanel.SetMinSize((150, 150)) |
|
358 if self.root and hasattr(self.root, "gcview_color_background"): |
|
359 self.glpanel.color_background = self.root.gcview_color_background |
|
360 self.clickcb = None |
|
361 self.widget = self.glpanel |
|
362 self.refresh_timer = wx.CallLater(100, self.Refresh) |
|
363 self.p = self # Hack for backwards compatibility with gviz API |
|
364 self.platform = actors.Platform(build_dimensions, circular = circular) |
|
365 self.model = None |
|
366 self.objects = [GCObject(self.platform), GCObject(None)] |
|
367 |
|
368 def __getattr__(self, name): |
|
369 return getattr(self.glpanel, name) |
|
370 |
|
371 def set_current_gline(self, gline): |
|
372 if gline.is_move and gline.gcview_end_vertex is not None \ |
|
373 and self.model and self.model.loaded: |
|
374 self.model.printed_until = gline.gcview_end_vertex |
|
375 if not self.refresh_timer.IsRunning(): |
|
376 self.refresh_timer.Start() |
|
377 |
|
378 def recreate_platform(self, build_dimensions, circular): |
|
379 return recreate_platform(self, build_dimensions, circular) |
|
380 |
|
381 def addgcodehighlight(self, *a): |
|
382 pass |
|
383 |
|
384 def setlayer(self, layer): |
|
385 if layer in self.model.layer_idxs_map: |
|
386 viz_layer = self.model.layer_idxs_map[layer] |
|
387 self.parent.model.num_layers_to_draw = viz_layer |
|
388 wx.CallAfter(self.Refresh) |
|
389 |
|
390 def clear(self): |
|
391 self.model = None |
|
392 self.objects[-1].model = None |
|
393 wx.CallAfter(self.Refresh) |
|
394 |
|
395 class GcodeViewFrame(GvizBaseFrame, GcodeViewLoader): |
|
396 '''A simple class for using OpenGL with wxPython.''' |
|
397 |
|
398 def __init__(self, parent, ID, title, build_dimensions, objects = None, |
|
399 pos = wx.DefaultPosition, size = wx.DefaultSize, |
|
400 style = wx.DEFAULT_FRAME_STYLE, root = None, circular = False, |
|
401 antialias_samples = 0): |
|
402 GvizBaseFrame.__init__(self, parent, ID, title, |
|
403 pos, size, style) |
|
404 self.root = root |
|
405 |
|
406 panel, vbox = self.create_base_ui() |
|
407 |
|
408 self.refresh_timer = wx.CallLater(100, self.Refresh) |
|
409 self.p = self # Hack for backwards compatibility with gviz API |
|
410 self.clonefrom = objects |
|
411 self.platform = actors.Platform(build_dimensions, circular = circular) |
|
412 if objects: |
|
413 self.model = objects[1].model |
|
414 else: |
|
415 self.model = None |
|
416 self.objects = [GCObject(self.platform), GCObject(None)] |
|
417 |
|
418 fit_image = wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap() |
|
419 self.toolbar.InsertLabelTool(6, 8, " " + _("Fit to plate"), fit_image, |
|
420 shortHelp = _("Fit to plate [F]"), |
|
421 longHelp = '') |
|
422 self.toolbar.Realize() |
|
423 self.glpanel = GcodeViewPanel(panel, |
|
424 build_dimensions = build_dimensions, |
|
425 realparent = self, |
|
426 antialias_samples = antialias_samples) |
|
427 vbox.Add(self.glpanel, 1, flag = wx.EXPAND) |
|
428 |
|
429 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1.2), id = 1) |
|
430 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1 / 1.2), id = 2) |
|
431 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.layerup(), id = 3) |
|
432 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.layerdown(), id = 4) |
|
433 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.resetview(), id = 5) |
|
434 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.fit(), id = 8) |
|
435 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.inject(), id = 6) |
|
436 self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.editlayer(), id = 7) |
|
437 |
|
438 def setlayercb(self, layer): |
|
439 self.layerslider.SetValue(layer) |
|
440 self.update_status("") |
|
441 |
|
442 def update_status(self, extra): |
|
443 layer = self.model.num_layers_to_draw |
|
444 filtered = [k for k, v in self.model.layer_idxs_map.iteritems() if v == layer] |
|
445 if filtered: |
|
446 true_layer = filtered[0] |
|
447 z = self.model.gcode.all_layers[true_layer].z |
|
448 message = _("Layer %d -%s Z = %.03f mm") % (layer, extra, z) |
|
449 else: |
|
450 message = _("Entire object") |
|
451 wx.CallAfter(self.SetStatusText, message, 0) |
|
452 |
|
453 def process_slider(self, event): |
|
454 new_layer = self.layerslider.GetValue() |
|
455 new_layer = min(self.model.max_layers + 1, new_layer) |
|
456 new_layer = max(1, new_layer) |
|
457 self.model.num_layers_to_draw = new_layer |
|
458 self.update_status("") |
|
459 wx.CallAfter(self.Refresh) |
|
460 |
|
461 def set_current_gline(self, gline): |
|
462 if gline.is_move and gline.gcview_end_vertex is not None \ |
|
463 and self.model and self.model.loaded: |
|
464 self.model.printed_until = gline.gcview_end_vertex |
|
465 if not self.refresh_timer.IsRunning(): |
|
466 self.refresh_timer.Start() |
|
467 |
|
468 def recreate_platform(self, build_dimensions, circular): |
|
469 return recreate_platform(self, build_dimensions, circular) |
|
470 |
|
471 def addfile(self, gcode = None): |
|
472 if self.clonefrom: |
|
473 self.model = self.clonefrom[-1].model.copy() |
|
474 self.objects[-1].model = self.model |
|
475 else: |
|
476 GcodeViewLoader.addfile(self, gcode) |
|
477 self.layerslider.SetRange(1, self.model.max_layers + 1) |
|
478 self.layerslider.SetValue(self.model.max_layers + 1) |
|
479 wx.CallAfter(self.SetStatusText, _("Entire object"), 0) |
|
480 wx.CallAfter(self.Refresh) |
|
481 |
|
482 def clear(self): |
|
483 self.model = None |
|
484 self.objects[-1].model = None |
|
485 wx.CallAfter(self.Refresh) |
|
486 |
|
487 if __name__ == "__main__": |
|
488 import sys |
|
489 app = wx.App(redirect = False) |
|
490 build_dimensions = [200, 200, 100, 0, 0, 0] |
|
491 title = 'Gcode view, shift to move view, mousewheel to set layer' |
|
492 frame = GcodeViewFrame(None, wx.ID_ANY, title, size = (400, 400), |
|
493 build_dimensions = build_dimensions) |
|
494 gcode = gcoder.GCode(open(sys.argv[1]), get_home_pos(build_dimensions)) |
|
495 frame.addfile(gcode) |
|
496 |
|
497 first_move = None |
|
498 for i in range(len(gcode.lines)): |
|
499 if gcode.lines[i].is_move: |
|
500 first_move = gcode.lines[i] |
|
501 break |
|
502 last_move = None |
|
503 for i in range(len(gcode.lines) - 1, -1, -1): |
|
504 if gcode.lines[i].is_move: |
|
505 last_move = gcode.lines[i] |
|
506 break |
|
507 nsteps = 20 |
|
508 steptime = 500 |
|
509 lines = [first_move] + [gcode.lines[int(float(i) * (len(gcode.lines) - 1) / nsteps)] for i in range(1, nsteps)] + [last_move] |
|
510 current_line = 0 |
|
511 |
|
512 def setLine(): |
|
513 global current_line |
|
514 frame.set_current_gline(lines[current_line]) |
|
515 current_line = (current_line + 1) % len(lines) |
|
516 timer.Start() |
|
517 timer = wx.CallLater(steptime, setLine) |
|
518 timer.Start() |
|
519 |
|
520 frame.Show(True) |
|
521 app.MainLoop() |
|
522 app.Destroy() |