printrun-src/printrun/gl/panel.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/printrun-src/printrun/gl/panel.py	Fri Jun 03 09:16:07 2016 +0200
@@ -0,0 +1,355 @@
+#!/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/>.
+
+from threading import Lock
+import logging
+import traceback
+import numpy
+import numpy.linalg
+
+import wx
+from wx import glcanvas
+
+import pyglet
+pyglet.options['debug_gl'] = True
+
+from pyglet.gl import glEnable, glDisable, GL_LIGHTING, glLightfv, \
+    GL_LIGHT0, GL_LIGHT1, GL_LIGHT2, GL_POSITION, GL_DIFFUSE, \
+    GL_AMBIENT, GL_SPECULAR, GL_COLOR_MATERIAL, \
+    glShadeModel, GL_SMOOTH, GL_NORMALIZE, \
+    GL_BLEND, glBlendFunc, glClear, glClearColor, \
+    glClearDepth, GL_COLOR_BUFFER_BIT, GL_CULL_FACE, \
+    GL_DEPTH_BUFFER_BIT, glDepthFunc, GL_DEPTH_TEST, \
+    GLdouble, glGetDoublev, glGetIntegerv, GLint, \
+    GL_LEQUAL, glLoadIdentity, glMatrixMode, GL_MODELVIEW, \
+    GL_MODELVIEW_MATRIX, GL_ONE_MINUS_SRC_ALPHA, glOrtho, \
+    GL_PROJECTION, GL_PROJECTION_MATRIX, glScalef, \
+    GL_SRC_ALPHA, glTranslatef, gluPerspective, gluUnProject, \
+    glViewport, GL_VIEWPORT
+from pyglet import gl
+from .trackball import trackball, mulquat
+from .libtatlin.actors import vec
+
+class wxGLPanel(wx.Panel):
+    '''A simple class for using OpenGL with wxPython.'''
+
+    orthographic = True
+    color_background = (0.98, 0.98, 0.78, 1)
+    do_lights = True
+
+    def __init__(self, parent, id, pos = wx.DefaultPosition,
+                 size = wx.DefaultSize, style = 0,
+                 antialias_samples = 0):
+        # Forcing a no full repaint to stop flickering
+        style = style | wx.NO_FULL_REPAINT_ON_RESIZE
+        super(wxGLPanel, self).__init__(parent, id, pos, size, style)
+
+        self.GLinitialized = False
+        self.mview_initialized = False
+        attribList = (glcanvas.WX_GL_RGBA,  # RGBA
+                      glcanvas.WX_GL_DOUBLEBUFFER,  # Double Buffered
+                      glcanvas.WX_GL_DEPTH_SIZE, 24)  # 24 bit
+
+        if antialias_samples > 0 and hasattr(glcanvas, "WX_GL_SAMPLE_BUFFERS"):
+            attribList += (glcanvas.WX_GL_SAMPLE_BUFFERS, 1,
+                           glcanvas.WX_GL_SAMPLES, antialias_samples)
+
+        self.width = None
+        self.height = None
+
+        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.canvas = glcanvas.GLCanvas(self, attribList = attribList)
+        self.context = glcanvas.GLContext(self.canvas)
+        self.sizer.Add(self.canvas, 1, wx.EXPAND)
+        self.SetSizerAndFit(self.sizer)
+
+        self.rot_lock = Lock()
+        self.basequat = [0, 0, 0, 1]
+        self.zoom_factor = 1.0
+
+        self.gl_broken = False
+
+        # bind events
+        self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent)
+        self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent)
+        self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent)
+
+    def processEraseBackgroundEvent(self, event):
+        '''Process the erase background event.'''
+        pass  # Do nothing, to avoid flashing on MSWin
+
+    def processSizeEvent(self, event):
+        '''Process the resize event.'''
+        if self.IsFrozen():
+            event.Skip()
+            return
+        if (wx.VERSION > (2, 9) and self.canvas.IsShownOnScreen()) or self.canvas.GetContext():
+            # Make sure the frame is shown before calling SetCurrent.
+            self.canvas.SetCurrent(self.context)
+            self.OnReshape()
+            self.Refresh(False)
+            timer = wx.CallLater(100, self.Refresh)
+            timer.Start()
+        event.Skip()
+
+    def processPaintEvent(self, event):
+        '''Process the drawing event.'''
+        self.canvas.SetCurrent(self.context)
+
+        if not self.gl_broken:
+            try:
+                self.OnInitGL()
+                self.OnDraw()
+            except pyglet.gl.lib.GLException:
+                self.gl_broken = True
+                logging.error(_("OpenGL failed, disabling it:")
+                              + "\n" + traceback.format_exc())
+        event.Skip()
+
+    def Destroy(self):
+        # clean up the pyglet OpenGL context
+        self.pygletcontext.destroy()
+        # call the super method
+        super(wxGLPanel, self).Destroy()
+
+    # ==========================================================================
+    # GLFrame OpenGL Event Handlers
+    # ==========================================================================
+    def OnInitGL(self, call_reshape = True):
+        '''Initialize OpenGL for use in the window.'''
+        if self.GLinitialized:
+            return
+        self.GLinitialized = True
+        # create a pyglet context for this panel
+        self.pygletcontext = gl.Context(gl.current_context)
+        self.pygletcontext.canvas = self
+        self.pygletcontext.set_current()
+        # normal gl init
+        glClearColor(*self.color_background)
+        glClearDepth(1.0)                # set depth value to 1
+        glDepthFunc(GL_LEQUAL)
+        glEnable(GL_COLOR_MATERIAL)
+        glEnable(GL_DEPTH_TEST)
+        glEnable(GL_CULL_FACE)
+        glEnable(GL_BLEND)
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+        if call_reshape:
+            self.OnReshape()
+
+    def OnReshape(self):
+        """Reshape the OpenGL viewport based on the size of the window"""
+        size = self.GetClientSize()
+        oldwidth, oldheight = self.width, self.height
+        width, height = size.width, size.height
+        if width < 1 or height < 1:
+            return
+        self.width = max(float(width), 1.0)
+        self.height = max(float(height), 1.0)
+        self.OnInitGL(call_reshape = False)
+        glViewport(0, 0, width, height)
+        glMatrixMode(GL_PROJECTION)
+        glLoadIdentity()
+        if self.orthographic:
+            glOrtho(-width / 2, width / 2, -height / 2, height / 2,
+                    -5 * self.dist, 5 * self.dist)
+        else:
+            gluPerspective(60., float(width) / height, 10.0, 3 * self.dist)
+            glTranslatef(0, 0, -self.dist)  # Move back
+        glMatrixMode(GL_MODELVIEW)
+
+        if not self.mview_initialized:
+            self.reset_mview(0.9)
+            self.mview_initialized = True
+        elif oldwidth is not None and oldheight is not None:
+            wratio = self.width / oldwidth
+            hratio = self.height / oldheight
+
+            factor = min(wratio * self.zoomed_width, hratio * self.zoomed_height)
+            x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2)
+            self.zoom(factor, (x, y))
+            self.zoomed_width *= wratio / factor
+            self.zoomed_height *= hratio / factor
+
+        # Wrap text to the width of the window
+        if self.GLinitialized:
+            self.pygletcontext.set_current()
+            self.update_object_resize()
+
+    def setup_lights(self):
+        if not self.do_lights:
+            return
+        glEnable(GL_LIGHTING)
+        glDisable(GL_LIGHT0)
+        glLightfv(GL_LIGHT0, GL_AMBIENT, vec(0.4, 0.4, 0.4, 1.0))
+        glLightfv(GL_LIGHT0, GL_SPECULAR, vec(0, 0, 0, 0))
+        glLightfv(GL_LIGHT0, GL_DIFFUSE, vec(0, 0, 0, 0))
+        glEnable(GL_LIGHT1)
+        glLightfv(GL_LIGHT1, GL_AMBIENT, vec(0, 0, 0, 1.0))
+        glLightfv(GL_LIGHT1, GL_SPECULAR, vec(0.6, 0.6, 0.6, 1.0))
+        glLightfv(GL_LIGHT2, GL_DIFFUSE, vec(0.8, 0.8, 0.8, 1))
+        glLightfv(GL_LIGHT1, GL_POSITION, vec(1, 2, 3, 0))
+        glEnable(GL_LIGHT2)
+        glLightfv(GL_LIGHT2, GL_AMBIENT, vec(0, 0, 0, 1.0))
+        glLightfv(GL_LIGHT2, GL_SPECULAR, vec(0.6, 0.6, 0.6, 1.0))
+        glLightfv(GL_LIGHT2, GL_DIFFUSE, vec(0.8, 0.8, 0.8, 1))
+        glLightfv(GL_LIGHT2, GL_POSITION, vec(-1, -1, 3, 0))
+        glEnable(GL_NORMALIZE)
+        glShadeModel(GL_SMOOTH)
+
+    def reset_mview(self, factor):
+        glMatrixMode(GL_MODELVIEW)
+        glLoadIdentity()
+        self.setup_lights()
+        if self.orthographic:
+            wratio = self.width / self.dist
+            hratio = self.height / self.dist
+            minratio = float(min(wratio, hratio))
+            self.zoom_factor = 1.0
+            self.zoomed_width = wratio / minratio
+            self.zoomed_height = hratio / minratio
+            glScalef(factor * minratio, factor * minratio, 1)
+
+    def OnDraw(self, *args, **kwargs):
+        """Draw the window."""
+        self.pygletcontext.set_current()
+        glClearColor(*self.color_background)
+        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+        self.draw_objects()
+        self.canvas.SwapBuffers()
+
+    # ==========================================================================
+    # To be implemented by a sub class
+    # ==========================================================================
+    def create_objects(self):
+        '''create opengl objects when opengl is initialized'''
+        pass
+
+    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'''
+        pass
+
+    # ==========================================================================
+    # Utils
+    # ==========================================================================
+    def get_modelview_mat(self, local_transform):
+        mvmat = (GLdouble * 16)()
+        glGetDoublev(GL_MODELVIEW_MATRIX, mvmat)
+        return mvmat
+
+    def mouse_to_3d(self, x, y, z = 1.0, local_transform = False):
+        x = float(x)
+        y = self.height - float(y)
+        # The following could work if we were not initially scaling to zoom on
+        # the bed
+        # if self.orthographic:
+        #    return (x - self.width / 2, y - self.height / 2, 0)
+        pmat = (GLdouble * 16)()
+        mvmat = self.get_modelview_mat(local_transform)
+        viewport = (GLint * 4)()
+        px = (GLdouble)()
+        py = (GLdouble)()
+        pz = (GLdouble)()
+        glGetIntegerv(GL_VIEWPORT, viewport)
+        glGetDoublev(GL_PROJECTION_MATRIX, pmat)
+        glGetDoublev(GL_MODELVIEW_MATRIX, mvmat)
+        gluUnProject(x, y, z, mvmat, pmat, viewport, px, py, pz)
+        return (px.value, py.value, pz.value)
+
+    def mouse_to_ray(self, x, y, local_transform = False):
+        x = float(x)
+        y = self.height - float(y)
+        pmat = (GLdouble * 16)()
+        mvmat = (GLdouble * 16)()
+        viewport = (GLint * 4)()
+        px = (GLdouble)()
+        py = (GLdouble)()
+        pz = (GLdouble)()
+        glGetIntegerv(GL_VIEWPORT, viewport)
+        glGetDoublev(GL_PROJECTION_MATRIX, pmat)
+        mvmat = self.get_modelview_mat(local_transform)
+        gluUnProject(x, y, 1, mvmat, pmat, viewport, px, py, pz)
+        ray_far = (px.value, py.value, pz.value)
+        gluUnProject(x, y, 0., mvmat, pmat, viewport, px, py, pz)
+        ray_near = (px.value, py.value, pz.value)
+        return ray_near, ray_far
+
+    def mouse_to_plane(self, x, y, plane_normal, plane_offset, local_transform = False):
+        # Ray/plane intersection
+        ray_near, ray_far = self.mouse_to_ray(x, y, local_transform)
+        ray_near = numpy.array(ray_near)
+        ray_far = numpy.array(ray_far)
+        ray_dir = ray_far - ray_near
+        ray_dir = ray_dir / numpy.linalg.norm(ray_dir)
+        plane_normal = numpy.array(plane_normal)
+        q = ray_dir.dot(plane_normal)
+        if q == 0:
+            return None
+        t = - (ray_near.dot(plane_normal) + plane_offset) / q
+        if t < 0:
+            return None
+        return ray_near + t * ray_dir
+
+    def zoom(self, factor, to = None):
+        glMatrixMode(GL_MODELVIEW)
+        if to:
+            delta_x = to[0]
+            delta_y = to[1]
+            glTranslatef(delta_x, delta_y, 0)
+        glScalef(factor, factor, 1)
+        self.zoom_factor *= factor
+        if to:
+            glTranslatef(-delta_x, -delta_y, 0)
+        wx.CallAfter(self.Refresh)
+
+    def zoom_to_center(self, factor):
+        self.canvas.SetCurrent(self.context)
+        x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2)
+        self.zoom(factor, (x, y))
+
+    def handle_rotation(self, event):
+        if self.initpos is None:
+            self.initpos = event.GetPositionTuple()
+        else:
+            p1 = self.initpos
+            p2 = event.GetPositionTuple()
+            sz = self.GetClientSize()
+            p1x = float(p1[0]) / (sz[0] / 2) - 1
+            p1y = 1 - float(p1[1]) / (sz[1] / 2)
+            p2x = float(p2[0]) / (sz[0] / 2) - 1
+            p2y = 1 - float(p2[1]) / (sz[1] / 2)
+            quat = trackball(p1x, p1y, p2x, p2y, self.dist / 250.0)
+            with self.rot_lock:
+                self.basequat = mulquat(self.basequat, quat)
+            self.initpos = p2
+
+    def handle_translation(self, event):
+        if self.initpos is None:
+            self.initpos = event.GetPositionTuple()
+        else:
+            p1 = self.initpos
+            p2 = event.GetPositionTuple()
+            if self.orthographic:
+                x1, y1, _ = self.mouse_to_3d(p1[0], p1[1])
+                x2, y2, _ = self.mouse_to_3d(p2[0], p2[1])
+                glTranslatef(x2 - x1, y2 - y1, 0)
+            else:
+                glTranslatef(p2[0] - p1[0], -(p2[1] - p1[1]), 0)
+            self.initpos = p2

mercurial