printrun-src/printrun/gcview.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
equal deleted inserted replaced
14:51bf56ba3c10 15:0bbb006204fc
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()

mercurial