--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/printrun-src/printrun/gcview.py Fri Jun 03 09:16:07 2016 +0200 @@ -0,0 +1,522 @@ +#!/usr/bin/env python + +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Printrun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Printrun. If not, see <http://www.gnu.org/licenses/>. + +import logging +import wx + +from . import gcoder +from .gl.panel import wxGLPanel +from .gl.trackball import build_rotmatrix +from .gl.libtatlin import actors +from .injectgcode import injector, injector_edit + +from pyglet.gl import glPushMatrix, glPopMatrix, \ + glTranslatef, glRotatef, glScalef, glMultMatrixd, \ + glGetDoublev, GL_MODELVIEW_MATRIX, GLdouble + +from .gviz import GvizBaseFrame + +from .utils import imagefile, install_locale, get_home_pos +install_locale('pronterface') + +def create_model(light): + if light: + return actors.GcodeModelLight() + else: + return actors.GcodeModel() + +def gcode_dims(g): + return ((g.xmin, g.xmax, g.width), + (g.ymin, g.ymax, g.depth), + (g.zmin, g.zmax, g.height)) + +def set_model_colors(model, root): + for field in dir(model): + if field.startswith("color_"): + root_fieldname = "gcview_" + field + if hasattr(root, root_fieldname): + setattr(model, field, getattr(root, root_fieldname)) + +def recreate_platform(self, build_dimensions, circular): + self.platform = actors.Platform(build_dimensions, circular = circular) + self.objects[0].model = self.platform + wx.CallAfter(self.Refresh) + +def set_gcview_params(self, path_width, path_height): + self.path_halfwidth = path_width / 2 + self.path_halfheight = path_height / 2 + has_changed = False + for obj in self.objects[1:]: + if isinstance(obj.model, actors.GcodeModel): + obj.model.set_path_size(self.path_halfwidth, self.path_halfheight) + has_changed = True + return has_changed + +class GcodeViewPanel(wxGLPanel): + + def __init__(self, parent, id = wx.ID_ANY, + build_dimensions = None, realparent = None, + antialias_samples = 0): + super(GcodeViewPanel, self).__init__(parent, id, wx.DefaultPosition, + wx.DefaultSize, 0, + antialias_samples = antialias_samples) + self.canvas.Bind(wx.EVT_MOUSE_EVENTS, self.move) + self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.double) + self.canvas.Bind(wx.EVT_KEY_DOWN, self.keypress) + self.initialized = 0 + self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.wheel) + self.parent = realparent if realparent else parent + self.initpos = None + if build_dimensions: + self.build_dimensions = build_dimensions + else: + self.build_dimensions = [200, 200, 100, 0, 0, 0] + self.dist = max(self.build_dimensions[0], self.build_dimensions[1]) + self.basequat = [0, 0, 0, 1] + self.mousepos = [0, 0] + + def inject(self): + l = self.parent.model.num_layers_to_draw + filtered = [k for k, v in self.parent.model.layer_idxs_map.iteritems() if v == l] + if filtered: + injector(self.parent.model.gcode, l, filtered[0]) + else: + logging.error(_("Invalid layer for injection")) + + def editlayer(self): + l = self.parent.model.num_layers_to_draw + filtered = [k for k, v in self.parent.model.layer_idxs_map.iteritems() if v == l] + if filtered: + injector_edit(self.parent.model.gcode, l, filtered[0]) + else: + logging.error(_("Invalid layer for edition")) + + def setlayercb(self, layer): + pass + + def OnInitGL(self, *args, **kwargs): + super(GcodeViewPanel, self).OnInitGL(*args, **kwargs) + if hasattr(self.parent, "filenames") and self.parent.filenames: + for filename in self.parent.filenames: + self.parent.load_file(filename) + self.parent.autoplate() + if hasattr(self.parent, "loadcb"): + self.parent.loadcb() + self.parent.filenames = None + + def create_objects(self): + '''create opengl objects when opengl is initialized''' + for obj in self.parent.objects: + if obj.model and obj.model.loaded and not obj.model.initialized: + obj.model.init() + + def update_object_resize(self): + '''called when the window recieves only if opengl is initialized''' + pass + + def draw_objects(self): + '''called in the middle of ondraw after the buffer has been cleared''' + self.create_objects() + + glPushMatrix() + # Rotate according to trackball + glMultMatrixd(build_rotmatrix(self.basequat)) + # Move origin to bottom left of platform + platformx0 = -self.build_dimensions[3] - self.parent.platform.width / 2 + platformy0 = -self.build_dimensions[4] - self.parent.platform.depth / 2 + glTranslatef(platformx0, platformy0, 0) + + for obj in self.parent.objects: + if not obj.model \ + or not obj.model.loaded \ + or not obj.model.initialized: + continue + glPushMatrix() + glTranslatef(*(obj.offsets)) + glRotatef(obj.rot, 0.0, 0.0, 1.0) + glTranslatef(*(obj.centeroffset)) + glScalef(*obj.scale) + + obj.model.display() + glPopMatrix() + glPopMatrix() + + # ========================================================================== + # Utils + # ========================================================================== + def get_modelview_mat(self, local_transform): + mvmat = (GLdouble * 16)() + if local_transform: + glPushMatrix() + # Rotate according to trackball + glMultMatrixd(build_rotmatrix(self.basequat)) + # Move origin to bottom left of platform + platformx0 = -self.build_dimensions[3] - self.parent.platform.width / 2 + platformy0 = -self.build_dimensions[4] - self.parent.platform.depth / 2 + glTranslatef(platformx0, platformy0, 0) + glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) + glPopMatrix() + else: + glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) + return mvmat + + def double(self, event): + if hasattr(self.parent, "clickcb") and self.parent.clickcb: + self.parent.clickcb(event) + + def move(self, event): + """react to mouse actions: + no mouse: show red mousedrop + LMB: rotate viewport + RMB: move viewport + """ + if event.Entering(): + self.canvas.SetFocus() + event.Skip() + return + if event.Dragging() and event.LeftIsDown(): + self.handle_rotation(event) + elif event.Dragging() and event.RightIsDown(): + self.handle_translation(event) + elif event.LeftUp(): + self.initpos = None + elif event.RightUp(): + self.initpos = None + else: + event.Skip() + return + event.Skip() + wx.CallAfter(self.Refresh) + + def layerup(self): + if not hasattr(self.parent, "model") or not self.parent.model: + return + max_layers = self.parent.model.max_layers + current_layer = self.parent.model.num_layers_to_draw + # accept going up to max_layers + 1 + # max_layers means visualizing the last layer differently, + # max_layers + 1 means visualizing all layers with the same color + new_layer = min(max_layers + 1, current_layer + 1) + self.parent.model.num_layers_to_draw = new_layer + self.parent.setlayercb(new_layer) + wx.CallAfter(self.Refresh) + + def layerdown(self): + if not hasattr(self.parent, "model") or not self.parent.model: + return + current_layer = self.parent.model.num_layers_to_draw + new_layer = max(1, current_layer - 1) + self.parent.model.num_layers_to_draw = new_layer + self.parent.setlayercb(new_layer) + wx.CallAfter(self.Refresh) + + def handle_wheel(self, event): + delta = event.GetWheelRotation() + factor = 1.05 + if event.ControlDown(): + factor = 1.02 + if hasattr(self.parent, "model") and event.ShiftDown(): + if not self.parent.model: + return + count = 1 if not event.ControlDown() else 10 + for i in range(count): + if delta > 0: self.layerup() + else: self.layerdown() + return + x, y = event.GetPositionTuple() + x, y, _ = self.mouse_to_3d(x, y) + if delta > 0: + self.zoom(factor, (x, y)) + else: + self.zoom(1 / factor, (x, y)) + + def wheel(self, event): + """react to mouse wheel actions: + without shift: set max layer + with shift: zoom viewport + """ + self.handle_wheel(event) + wx.CallAfter(self.Refresh) + + def fit(self): + if not self.parent.model or not self.parent.model.loaded: + return + self.canvas.SetCurrent(self.context) + dims = gcode_dims(self.parent.model.gcode) + self.reset_mview(1.0) + center_x = (dims[0][0] + dims[0][1]) / 2 + center_y = (dims[1][0] + dims[1][1]) / 2 + center_x = self.build_dimensions[0] / 2 - center_x + center_y = self.build_dimensions[1] / 2 - center_y + if self.orthographic: + ratio = float(self.dist) / max(dims[0][2], dims[1][2]) + glScalef(ratio, ratio, 1) + glTranslatef(center_x, center_y, 0) + wx.CallAfter(self.Refresh) + + def keypress(self, event): + """gets keypress events and moves/rotates acive shape""" + step = 1.1 + if event.ControlDown(): + step = 1.05 + kup = [85, 315] # Up keys + kdo = [68, 317] # Down Keys + kzi = [wx.WXK_PAGEDOWN, 388, 316, 61] # Zoom In Keys + kzo = [wx.WXK_PAGEUP, 390, 314, 45] # Zoom Out Keys + kfit = [70] # Fit to print keys + kshowcurrent = [67] # Show only current layer keys + kreset = [82] # Reset keys + key = event.GetKeyCode() + if key in kup: + self.layerup() + if key in kdo: + self.layerdown() + x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) + if key in kzi: + self.zoom_to_center(step) + if key in kzo: + self.zoom_to_center(1 / step) + if key in kfit: + self.fit() + if key in kshowcurrent: + if not self.parent.model or not self.parent.model.loaded: + return + self.parent.model.only_current = not self.parent.model.only_current + wx.CallAfter(self.Refresh) + if key in kreset: + self.resetview() + event.Skip() + + def resetview(self): + self.canvas.SetCurrent(self.context) + self.reset_mview(0.9) + self.basequat = [0, 0, 0, 1] + wx.CallAfter(self.Refresh) + +class GCObject(object): + + def __init__(self, model): + self.offsets = [0, 0, 0] + self.centeroffset = [0, 0, 0] + self.rot = 0 + self.curlayer = 0.0 + self.scale = [1.0, 1.0, 1.0] + self.model = model + +class GcodeViewLoader(object): + + path_halfwidth = 0.2 + path_halfheight = 0.15 + + def addfile_perlayer(self, gcode = None, showall = False): + self.model = create_model(self.root.settings.light3d + if self.root else False) + if isinstance(self.model, actors.GcodeModel): + self.model.set_path_size(self.path_halfwidth, self.path_halfheight) + self.objects[-1].model = self.model + if self.root: + set_model_colors(self.model, self.root) + if gcode is not None: + generator = self.model.load_data(gcode) + generator_output = generator.next() + while generator_output is not None: + yield generator_output + generator_output = generator.next() + wx.CallAfter(self.Refresh) + yield None + + def addfile(self, gcode = None, showall = False): + generator = self.addfile_perlayer(gcode, showall) + while generator.next() is not None: + continue + + def set_gcview_params(self, path_width, path_height): + return set_gcview_params(self, path_width, path_height) + +class GcodeViewMainWrapper(GcodeViewLoader): + + def __init__(self, parent, build_dimensions, root, circular, antialias_samples): + self.root = root + self.glpanel = GcodeViewPanel(parent, realparent = self, + build_dimensions = build_dimensions, + antialias_samples = antialias_samples) + self.glpanel.SetMinSize((150, 150)) + if self.root and hasattr(self.root, "gcview_color_background"): + self.glpanel.color_background = self.root.gcview_color_background + self.clickcb = None + self.widget = self.glpanel + self.refresh_timer = wx.CallLater(100, self.Refresh) + self.p = self # Hack for backwards compatibility with gviz API + self.platform = actors.Platform(build_dimensions, circular = circular) + self.model = None + self.objects = [GCObject(self.platform), GCObject(None)] + + def __getattr__(self, name): + return getattr(self.glpanel, name) + + def set_current_gline(self, gline): + if gline.is_move and gline.gcview_end_vertex is not None \ + and self.model and self.model.loaded: + self.model.printed_until = gline.gcview_end_vertex + if not self.refresh_timer.IsRunning(): + self.refresh_timer.Start() + + def recreate_platform(self, build_dimensions, circular): + return recreate_platform(self, build_dimensions, circular) + + def addgcodehighlight(self, *a): + pass + + def setlayer(self, layer): + if layer in self.model.layer_idxs_map: + viz_layer = self.model.layer_idxs_map[layer] + self.parent.model.num_layers_to_draw = viz_layer + wx.CallAfter(self.Refresh) + + def clear(self): + self.model = None + self.objects[-1].model = None + wx.CallAfter(self.Refresh) + +class GcodeViewFrame(GvizBaseFrame, GcodeViewLoader): + '''A simple class for using OpenGL with wxPython.''' + + def __init__(self, parent, ID, title, build_dimensions, objects = None, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.DEFAULT_FRAME_STYLE, root = None, circular = False, + antialias_samples = 0): + GvizBaseFrame.__init__(self, parent, ID, title, + pos, size, style) + self.root = root + + panel, vbox = self.create_base_ui() + + self.refresh_timer = wx.CallLater(100, self.Refresh) + self.p = self # Hack for backwards compatibility with gviz API + self.clonefrom = objects + self.platform = actors.Platform(build_dimensions, circular = circular) + if objects: + self.model = objects[1].model + else: + self.model = None + self.objects = [GCObject(self.platform), GCObject(None)] + + fit_image = wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap() + self.toolbar.InsertLabelTool(6, 8, " " + _("Fit to plate"), fit_image, + shortHelp = _("Fit to plate [F]"), + longHelp = '') + self.toolbar.Realize() + self.glpanel = GcodeViewPanel(panel, + build_dimensions = build_dimensions, + realparent = self, + antialias_samples = antialias_samples) + vbox.Add(self.glpanel, 1, flag = wx.EXPAND) + + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1.2), id = 1) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1 / 1.2), id = 2) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.layerup(), id = 3) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.layerdown(), id = 4) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.resetview(), id = 5) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.fit(), id = 8) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.inject(), id = 6) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.editlayer(), id = 7) + + def setlayercb(self, layer): + self.layerslider.SetValue(layer) + self.update_status("") + + def update_status(self, extra): + layer = self.model.num_layers_to_draw + filtered = [k for k, v in self.model.layer_idxs_map.iteritems() if v == layer] + if filtered: + true_layer = filtered[0] + z = self.model.gcode.all_layers[true_layer].z + message = _("Layer %d -%s Z = %.03f mm") % (layer, extra, z) + else: + message = _("Entire object") + wx.CallAfter(self.SetStatusText, message, 0) + + def process_slider(self, event): + new_layer = self.layerslider.GetValue() + new_layer = min(self.model.max_layers + 1, new_layer) + new_layer = max(1, new_layer) + self.model.num_layers_to_draw = new_layer + self.update_status("") + wx.CallAfter(self.Refresh) + + def set_current_gline(self, gline): + if gline.is_move and gline.gcview_end_vertex is not None \ + and self.model and self.model.loaded: + self.model.printed_until = gline.gcview_end_vertex + if not self.refresh_timer.IsRunning(): + self.refresh_timer.Start() + + def recreate_platform(self, build_dimensions, circular): + return recreate_platform(self, build_dimensions, circular) + + def addfile(self, gcode = None): + if self.clonefrom: + self.model = self.clonefrom[-1].model.copy() + self.objects[-1].model = self.model + else: + GcodeViewLoader.addfile(self, gcode) + self.layerslider.SetRange(1, self.model.max_layers + 1) + self.layerslider.SetValue(self.model.max_layers + 1) + wx.CallAfter(self.SetStatusText, _("Entire object"), 0) + wx.CallAfter(self.Refresh) + + def clear(self): + self.model = None + self.objects[-1].model = None + wx.CallAfter(self.Refresh) + +if __name__ == "__main__": + import sys + app = wx.App(redirect = False) + build_dimensions = [200, 200, 100, 0, 0, 0] + title = 'Gcode view, shift to move view, mousewheel to set layer' + frame = GcodeViewFrame(None, wx.ID_ANY, title, size = (400, 400), + build_dimensions = build_dimensions) + gcode = gcoder.GCode(open(sys.argv[1]), get_home_pos(build_dimensions)) + frame.addfile(gcode) + + first_move = None + for i in range(len(gcode.lines)): + if gcode.lines[i].is_move: + first_move = gcode.lines[i] + break + last_move = None + for i in range(len(gcode.lines) - 1, -1, -1): + if gcode.lines[i].is_move: + last_move = gcode.lines[i] + break + nsteps = 20 + steptime = 500 + lines = [first_move] + [gcode.lines[int(float(i) * (len(gcode.lines) - 1) / nsteps)] for i in range(1, nsteps)] + [last_move] + current_line = 0 + + def setLine(): + global current_line + frame.set_current_gline(lines[current_line]) + current_line = (current_line + 1) % len(lines) + timer.Start() + timer = wx.CallLater(steptime, setLine) + timer.Start() + + frame.Show(True) + app.MainLoop() + app.Destroy()