printrun-src/printrun/gcview.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
--- /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()

mercurial