--- a/printrun-src/printrun/gl/panel.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gl/panel.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -39,81 +37,136 @@ GL_MODELVIEW_MATRIX, GL_ONE_MINUS_SRC_ALPHA, glOrtho, \ GL_PROJECTION, GL_PROJECTION_MATRIX, glScalef, \ GL_SRC_ALPHA, glTranslatef, gluPerspective, gluUnProject, \ - glViewport, GL_VIEWPORT + glViewport, GL_VIEWPORT, glPushMatrix, glPopMatrix, \ + glBegin, glVertex2f, glVertex3f, glEnd, GL_LINE_LOOP, glColor3f, \ + GL_LINE_STIPPLE, glColor4f, glLineStipple + from pyglet import gl -from .trackball import trackball, mulquat +from .trackball import trackball, mulquat, axis_to_quat from .libtatlin.actors import vec +from pyglet.gl.glu import gluOrtho2D -class wxGLPanel(wx.Panel): +# When Subclassing wx.Window in Windows the focus goes to the wx.Window +# instead of GLCanvas and it does not draw the focus rectangle and +# does not consume used keystrokes +# BASE_CLASS = wx.Window +# Subclassing Panel solves problem In Windows +BASE_CLASS = wx.Panel +# BASE_CLASS = wx.ScrolledWindow +# BASE_CLASS = glcanvas.GLCanvas +class wxGLPanel(BASE_CLASS): '''A simple class for using OpenGL with wxPython.''' + orbit_control = True orthographic = True color_background = (0.98, 0.98, 0.78, 1) do_lights = True - def __init__(self, parent, id, pos = wx.DefaultPosition, + def __init__(self, parent, 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) + # Full repaint should not be a performance problem + #TODO: test on windows, tested in Ubuntu + style = style | wx.FULL_REPAINT_ON_RESIZE self.GLinitialized = False self.mview_initialized = False - attribList = (glcanvas.WX_GL_RGBA, # RGBA + attribList = [glcanvas.WX_GL_RGBA, # RGBA glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered - glcanvas.WX_GL_DEPTH_SIZE, 24) # 24 bit + 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 + attribList.append(0) - self.sizer = wx.BoxSizer(wx.HORIZONTAL) - self.canvas = glcanvas.GLCanvas(self, attribList = attribList) + if BASE_CLASS is glcanvas.GLCanvas: + super().__init__(parent, wx.ID_ANY, attribList, pos, size, style) + self.canvas = self + else: + super().__init__(parent, wx.ID_ANY, pos, size, style) + self.canvas = glcanvas.GLCanvas(self, wx.ID_ANY, attribList, pos, size, style) + + self.width = self.height = None + 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.angle_z = 0 + self.angle_x = 0 self.gl_broken = False # bind events + self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) + if self.canvas is not self: + self.Bind(wx.EVT_SIZE, self.OnScrollSize) + # do not focus parent (panel like) but its canvas + self.SetCanFocus(False) + self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent) - self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) + # In wxWidgets 3.0.x there is a clipping bug during resizing + # which could be affected by painting the container + # self.Bind(wx.EVT_PAINT, self.processPaintEvent) + # Upgrade to wxPython 4.1 recommended self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) + self.canvas.Bind(wx.EVT_SET_FOCUS, self.processFocus) + self.canvas.Bind(wx.EVT_KILL_FOCUS, self.processKillFocus) + + def processFocus(self, ev): + # print('processFocus') + self.Refresh(False) + ev.Skip() + + def processKillFocus(self, ev): + # print('processKillFocus') + self.Refresh(False) + ev.Skip() + # def processIdle(self, event): + # print('processIdle') + # event.Skip() + + def Layout(self): + return super().Layout() + + def Refresh(self, eraseback=True): + # print('Refresh') + return super().Refresh(eraseback) + + def OnScrollSize(self, event): + self.canvas.SetSize(event.Size) + 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(): + + # print('processSizeEvent frozen', self.IsFrozen(), event.Size.x, self.ClientSize.x) + if not self.IsFrozen() and self.canvas.IsShownOnScreen(): # 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() + + # self.Refresh(False) + # print('Refresh') event.Skip() def processPaintEvent(self, event): '''Process the drawing event.''' + # print('wxGLPanel.processPaintEvent', self.ClientSize.Width) self.canvas.SetCurrent(self.context) if not self.gl_broken: try: self.OnInitGL() - self.OnDraw() + self.DrawCanvas() except pyglet.gl.lib.GLException: self.gl_broken = True logging.error(_("OpenGL failed, disabling it:") @@ -124,7 +177,7 @@ # clean up the pyglet OpenGL context self.pygletcontext.destroy() # call the super method - super(wxGLPanel, self).Destroy() + super().Destroy() # ========================================================================== # GLFrame OpenGL Event Handlers @@ -160,6 +213,7 @@ self.width = max(float(width), 1.0) self.height = max(float(height), 1.0) self.OnInitGL(call_reshape = False) + # print('glViewport', width) glViewport(0, 0, width, height) glMatrixMode(GL_PROJECTION) glLoadIdentity() @@ -223,13 +277,46 @@ self.zoomed_height = hratio / minratio glScalef(factor * minratio, factor * minratio, 1) - def OnDraw(self, *args, **kwargs): + def DrawCanvas(self): """Draw the window.""" + #import time + #start = time.perf_counter() + # print('DrawCanvas', self.canvas.GetClientRect()) self.pygletcontext.set_current() glClearColor(*self.color_background) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self.draw_objects() + + if self.canvas.HasFocus(): + self.drawFocus() self.canvas.SwapBuffers() + #print('Draw took', '%.2f'%(time.perf_counter()-start)) + + def drawFocus(self): + glColor4f(0, 0, 0, 0.4) + + glPushMatrix() + glLoadIdentity() + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, self.width, 0, self.height) + + glLineStipple(1, 0xf0f0) + glEnable(GL_LINE_STIPPLE) + glBegin(GL_LINE_LOOP) + glVertex2f(1, 0) + glVertex2f(self.width, 0) + glVertex2f(self.width, self.height-1) + glVertex2f(1, self.height-1) + glEnd() + glDisable(GL_LINE_STIPPLE) + + glPopMatrix() # restore PROJECTION + + glMatrixMode(GL_MODELVIEW) + glPopMatrix() # ========================================================================== # To be implemented by a sub class @@ -317,35 +404,54 @@ self.zoom_factor *= factor if to: glTranslatef(-delta_x, -delta_y, 0) - wx.CallAfter(self.Refresh) + # For wxPython (<4.1) and GTK: + # when you resize (enlarge) 3d view fast towards the log pane + # sash garbage may remain in GLCanvas + # The following refresh clears it at the cost of + # doubled frame draws. + # wx.CallAfter(self.Refresh) + self.Refresh(False) 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 orbit(self, p1x, p1y, p2x, p2y): + rz = p2x-p1x + self.angle_z-=rz + rotz = axis_to_quat([0.0,0.0,1.0],self.angle_z) + + rx = p2y-p1y + self.angle_x+=rx + rota = axis_to_quat([1.0,0.0,0.0],self.angle_x) + return mulquat(rotz,rota) + def handle_rotation(self, event): if self.initpos is None: - self.initpos = event.GetPositionTuple() + self.initpos = event.GetPosition() else: p1 = self.initpos - p2 = event.GetPositionTuple() + p2 = event.GetPosition() 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) + p1x = p1[0] / (sz[0] / 2) - 1 + p1y = 1 - p1[1] / (sz[1] / 2) + p2x = p2[0] / (sz[0] / 2) - 1 + p2y = 1 - 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) + if self.orbit_control: + self.basequat = self.orbit(p1x, p1y, p2x, p2y) + else: + self.basequat = mulquat(self.basequat, quat) self.initpos = p2 def handle_translation(self, event): if self.initpos is None: - self.initpos = event.GetPositionTuple() + self.initpos = event.GetPosition() else: p1 = self.initpos - p2 = event.GetPositionTuple() + p2 = event.GetPosition() if self.orthographic: x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) x2, y2, _ = self.mouse_to_3d(p2[0], p2[1])