Thu, 30 May 2019 19:02:52 +0200
bugfixes on rastered image column endings, force laser off with G0 + little offset
15 | 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() |