37 GLdouble, glGetDoublev, glGetIntegerv, GLint, \ |
35 GLdouble, glGetDoublev, glGetIntegerv, GLint, \ |
38 GL_LEQUAL, glLoadIdentity, glMatrixMode, GL_MODELVIEW, \ |
36 GL_LEQUAL, glLoadIdentity, glMatrixMode, GL_MODELVIEW, \ |
39 GL_MODELVIEW_MATRIX, GL_ONE_MINUS_SRC_ALPHA, glOrtho, \ |
37 GL_MODELVIEW_MATRIX, GL_ONE_MINUS_SRC_ALPHA, glOrtho, \ |
40 GL_PROJECTION, GL_PROJECTION_MATRIX, glScalef, \ |
38 GL_PROJECTION, GL_PROJECTION_MATRIX, glScalef, \ |
41 GL_SRC_ALPHA, glTranslatef, gluPerspective, gluUnProject, \ |
39 GL_SRC_ALPHA, glTranslatef, gluPerspective, gluUnProject, \ |
42 glViewport, GL_VIEWPORT |
40 glViewport, GL_VIEWPORT, glPushMatrix, glPopMatrix, \ |
|
41 glBegin, glVertex2f, glVertex3f, glEnd, GL_LINE_LOOP, glColor3f, \ |
|
42 GL_LINE_STIPPLE, glColor4f, glLineStipple |
|
43 |
43 from pyglet import gl |
44 from pyglet import gl |
44 from .trackball import trackball, mulquat |
45 from .trackball import trackball, mulquat, axis_to_quat |
45 from .libtatlin.actors import vec |
46 from .libtatlin.actors import vec |
46 |
47 from pyglet.gl.glu import gluOrtho2D |
47 class wxGLPanel(wx.Panel): |
48 |
|
49 # When Subclassing wx.Window in Windows the focus goes to the wx.Window |
|
50 # instead of GLCanvas and it does not draw the focus rectangle and |
|
51 # does not consume used keystrokes |
|
52 # BASE_CLASS = wx.Window |
|
53 # Subclassing Panel solves problem In Windows |
|
54 BASE_CLASS = wx.Panel |
|
55 # BASE_CLASS = wx.ScrolledWindow |
|
56 # BASE_CLASS = glcanvas.GLCanvas |
|
57 class wxGLPanel(BASE_CLASS): |
48 '''A simple class for using OpenGL with wxPython.''' |
58 '''A simple class for using OpenGL with wxPython.''' |
49 |
59 |
|
60 orbit_control = True |
50 orthographic = True |
61 orthographic = True |
51 color_background = (0.98, 0.98, 0.78, 1) |
62 color_background = (0.98, 0.98, 0.78, 1) |
52 do_lights = True |
63 do_lights = True |
53 |
64 |
54 def __init__(self, parent, id, pos = wx.DefaultPosition, |
65 def __init__(self, parent, pos = wx.DefaultPosition, |
55 size = wx.DefaultSize, style = 0, |
66 size = wx.DefaultSize, style = 0, |
56 antialias_samples = 0): |
67 antialias_samples = 0): |
57 # Forcing a no full repaint to stop flickering |
68 # Full repaint should not be a performance problem |
58 style = style | wx.NO_FULL_REPAINT_ON_RESIZE |
69 #TODO: test on windows, tested in Ubuntu |
59 super(wxGLPanel, self).__init__(parent, id, pos, size, style) |
70 style = style | wx.FULL_REPAINT_ON_RESIZE |
60 |
71 |
61 self.GLinitialized = False |
72 self.GLinitialized = False |
62 self.mview_initialized = False |
73 self.mview_initialized = False |
63 attribList = (glcanvas.WX_GL_RGBA, # RGBA |
74 attribList = [glcanvas.WX_GL_RGBA, # RGBA |
64 glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered |
75 glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered |
65 glcanvas.WX_GL_DEPTH_SIZE, 24) # 24 bit |
76 glcanvas.WX_GL_DEPTH_SIZE, 24 # 24 bit |
|
77 ] |
66 |
78 |
67 if antialias_samples > 0 and hasattr(glcanvas, "WX_GL_SAMPLE_BUFFERS"): |
79 if antialias_samples > 0 and hasattr(glcanvas, "WX_GL_SAMPLE_BUFFERS"): |
68 attribList += (glcanvas.WX_GL_SAMPLE_BUFFERS, 1, |
80 attribList += (glcanvas.WX_GL_SAMPLE_BUFFERS, 1, |
69 glcanvas.WX_GL_SAMPLES, antialias_samples) |
81 glcanvas.WX_GL_SAMPLES, antialias_samples) |
70 |
82 |
71 self.width = None |
83 attribList.append(0) |
72 self.height = None |
84 |
73 |
85 if BASE_CLASS is glcanvas.GLCanvas: |
74 self.sizer = wx.BoxSizer(wx.HORIZONTAL) |
86 super().__init__(parent, wx.ID_ANY, attribList, pos, size, style) |
75 self.canvas = glcanvas.GLCanvas(self, attribList = attribList) |
87 self.canvas = self |
|
88 else: |
|
89 super().__init__(parent, wx.ID_ANY, pos, size, style) |
|
90 self.canvas = glcanvas.GLCanvas(self, wx.ID_ANY, attribList, pos, size, style) |
|
91 |
|
92 self.width = self.height = None |
|
93 |
76 self.context = glcanvas.GLContext(self.canvas) |
94 self.context = glcanvas.GLContext(self.canvas) |
77 self.sizer.Add(self.canvas, 1, wx.EXPAND) |
|
78 self.SetSizerAndFit(self.sizer) |
|
79 |
95 |
80 self.rot_lock = Lock() |
96 self.rot_lock = Lock() |
81 self.basequat = [0, 0, 0, 1] |
97 self.basequat = [0, 0, 0, 1] |
82 self.zoom_factor = 1.0 |
98 self.zoom_factor = 1.0 |
|
99 self.angle_z = 0 |
|
100 self.angle_x = 0 |
83 |
101 |
84 self.gl_broken = False |
102 self.gl_broken = False |
85 |
103 |
86 # bind events |
104 # bind events |
|
105 self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) |
|
106 if self.canvas is not self: |
|
107 self.Bind(wx.EVT_SIZE, self.OnScrollSize) |
|
108 # do not focus parent (panel like) but its canvas |
|
109 self.SetCanFocus(False) |
|
110 |
87 self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent) |
111 self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent) |
88 self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) |
112 # In wxWidgets 3.0.x there is a clipping bug during resizing |
|
113 # which could be affected by painting the container |
|
114 # self.Bind(wx.EVT_PAINT, self.processPaintEvent) |
|
115 # Upgrade to wxPython 4.1 recommended |
89 self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) |
116 self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) |
|
117 |
|
118 self.canvas.Bind(wx.EVT_SET_FOCUS, self.processFocus) |
|
119 self.canvas.Bind(wx.EVT_KILL_FOCUS, self.processKillFocus) |
|
120 |
|
121 def processFocus(self, ev): |
|
122 # print('processFocus') |
|
123 self.Refresh(False) |
|
124 ev.Skip() |
|
125 |
|
126 def processKillFocus(self, ev): |
|
127 # print('processKillFocus') |
|
128 self.Refresh(False) |
|
129 ev.Skip() |
|
130 # def processIdle(self, event): |
|
131 # print('processIdle') |
|
132 # event.Skip() |
|
133 |
|
134 def Layout(self): |
|
135 return super().Layout() |
|
136 |
|
137 def Refresh(self, eraseback=True): |
|
138 # print('Refresh') |
|
139 return super().Refresh(eraseback) |
|
140 |
|
141 def OnScrollSize(self, event): |
|
142 self.canvas.SetSize(event.Size) |
90 |
143 |
91 def processEraseBackgroundEvent(self, event): |
144 def processEraseBackgroundEvent(self, event): |
92 '''Process the erase background event.''' |
145 '''Process the erase background event.''' |
93 pass # Do nothing, to avoid flashing on MSWin |
146 pass # Do nothing, to avoid flashing on MSWin |
94 |
147 |
95 def processSizeEvent(self, event): |
148 def processSizeEvent(self, event): |
96 '''Process the resize event.''' |
149 '''Process the resize event.''' |
97 if self.IsFrozen(): |
150 |
98 event.Skip() |
151 # print('processSizeEvent frozen', self.IsFrozen(), event.Size.x, self.ClientSize.x) |
99 return |
152 if not self.IsFrozen() and self.canvas.IsShownOnScreen(): |
100 if (wx.VERSION > (2, 9) and self.canvas.IsShownOnScreen()) or self.canvas.GetContext(): |
|
101 # Make sure the frame is shown before calling SetCurrent. |
153 # Make sure the frame is shown before calling SetCurrent. |
102 self.canvas.SetCurrent(self.context) |
154 self.canvas.SetCurrent(self.context) |
103 self.OnReshape() |
155 self.OnReshape() |
104 self.Refresh(False) |
156 |
105 timer = wx.CallLater(100, self.Refresh) |
157 # self.Refresh(False) |
106 timer.Start() |
158 # print('Refresh') |
107 event.Skip() |
159 event.Skip() |
108 |
160 |
109 def processPaintEvent(self, event): |
161 def processPaintEvent(self, event): |
110 '''Process the drawing event.''' |
162 '''Process the drawing event.''' |
|
163 # print('wxGLPanel.processPaintEvent', self.ClientSize.Width) |
111 self.canvas.SetCurrent(self.context) |
164 self.canvas.SetCurrent(self.context) |
112 |
165 |
113 if not self.gl_broken: |
166 if not self.gl_broken: |
114 try: |
167 try: |
115 self.OnInitGL() |
168 self.OnInitGL() |
116 self.OnDraw() |
169 self.DrawCanvas() |
117 except pyglet.gl.lib.GLException: |
170 except pyglet.gl.lib.GLException: |
118 self.gl_broken = True |
171 self.gl_broken = True |
119 logging.error(_("OpenGL failed, disabling it:") |
172 logging.error(_("OpenGL failed, disabling it:") |
120 + "\n" + traceback.format_exc()) |
173 + "\n" + traceback.format_exc()) |
121 event.Skip() |
174 event.Skip() |
122 |
175 |
123 def Destroy(self): |
176 def Destroy(self): |
124 # clean up the pyglet OpenGL context |
177 # clean up the pyglet OpenGL context |
125 self.pygletcontext.destroy() |
178 self.pygletcontext.destroy() |
126 # call the super method |
179 # call the super method |
127 super(wxGLPanel, self).Destroy() |
180 super().Destroy() |
128 |
181 |
129 # ========================================================================== |
182 # ========================================================================== |
130 # GLFrame OpenGL Event Handlers |
183 # GLFrame OpenGL Event Handlers |
131 # ========================================================================== |
184 # ========================================================================== |
132 def OnInitGL(self, call_reshape = True): |
185 def OnInitGL(self, call_reshape = True): |
221 self.zoom_factor = 1.0 |
275 self.zoom_factor = 1.0 |
222 self.zoomed_width = wratio / minratio |
276 self.zoomed_width = wratio / minratio |
223 self.zoomed_height = hratio / minratio |
277 self.zoomed_height = hratio / minratio |
224 glScalef(factor * minratio, factor * minratio, 1) |
278 glScalef(factor * minratio, factor * minratio, 1) |
225 |
279 |
226 def OnDraw(self, *args, **kwargs): |
280 def DrawCanvas(self): |
227 """Draw the window.""" |
281 """Draw the window.""" |
|
282 #import time |
|
283 #start = time.perf_counter() |
|
284 # print('DrawCanvas', self.canvas.GetClientRect()) |
228 self.pygletcontext.set_current() |
285 self.pygletcontext.set_current() |
229 glClearColor(*self.color_background) |
286 glClearColor(*self.color_background) |
230 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) |
287 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) |
231 self.draw_objects() |
288 self.draw_objects() |
|
289 |
|
290 if self.canvas.HasFocus(): |
|
291 self.drawFocus() |
232 self.canvas.SwapBuffers() |
292 self.canvas.SwapBuffers() |
|
293 #print('Draw took', '%.2f'%(time.perf_counter()-start)) |
|
294 |
|
295 def drawFocus(self): |
|
296 glColor4f(0, 0, 0, 0.4) |
|
297 |
|
298 glPushMatrix() |
|
299 glLoadIdentity() |
|
300 |
|
301 glMatrixMode(GL_PROJECTION) |
|
302 glPushMatrix() |
|
303 glLoadIdentity() |
|
304 gluOrtho2D(0, self.width, 0, self.height) |
|
305 |
|
306 glLineStipple(1, 0xf0f0) |
|
307 glEnable(GL_LINE_STIPPLE) |
|
308 glBegin(GL_LINE_LOOP) |
|
309 glVertex2f(1, 0) |
|
310 glVertex2f(self.width, 0) |
|
311 glVertex2f(self.width, self.height-1) |
|
312 glVertex2f(1, self.height-1) |
|
313 glEnd() |
|
314 glDisable(GL_LINE_STIPPLE) |
|
315 |
|
316 glPopMatrix() # restore PROJECTION |
|
317 |
|
318 glMatrixMode(GL_MODELVIEW) |
|
319 glPopMatrix() |
233 |
320 |
234 # ========================================================================== |
321 # ========================================================================== |
235 # To be implemented by a sub class |
322 # To be implemented by a sub class |
236 # ========================================================================== |
323 # ========================================================================== |
237 def create_objects(self): |
324 def create_objects(self): |
315 glTranslatef(delta_x, delta_y, 0) |
402 glTranslatef(delta_x, delta_y, 0) |
316 glScalef(factor, factor, 1) |
403 glScalef(factor, factor, 1) |
317 self.zoom_factor *= factor |
404 self.zoom_factor *= factor |
318 if to: |
405 if to: |
319 glTranslatef(-delta_x, -delta_y, 0) |
406 glTranslatef(-delta_x, -delta_y, 0) |
320 wx.CallAfter(self.Refresh) |
407 # For wxPython (<4.1) and GTK: |
|
408 # when you resize (enlarge) 3d view fast towards the log pane |
|
409 # sash garbage may remain in GLCanvas |
|
410 # The following refresh clears it at the cost of |
|
411 # doubled frame draws. |
|
412 # wx.CallAfter(self.Refresh) |
|
413 self.Refresh(False) |
321 |
414 |
322 def zoom_to_center(self, factor): |
415 def zoom_to_center(self, factor): |
323 self.canvas.SetCurrent(self.context) |
416 self.canvas.SetCurrent(self.context) |
324 x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) |
417 x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) |
325 self.zoom(factor, (x, y)) |
418 self.zoom(factor, (x, y)) |
326 |
419 |
|
420 def orbit(self, p1x, p1y, p2x, p2y): |
|
421 rz = p2x-p1x |
|
422 self.angle_z-=rz |
|
423 rotz = axis_to_quat([0.0,0.0,1.0],self.angle_z) |
|
424 |
|
425 rx = p2y-p1y |
|
426 self.angle_x+=rx |
|
427 rota = axis_to_quat([1.0,0.0,0.0],self.angle_x) |
|
428 return mulquat(rotz,rota) |
|
429 |
327 def handle_rotation(self, event): |
430 def handle_rotation(self, event): |
328 if self.initpos is None: |
431 if self.initpos is None: |
329 self.initpos = event.GetPositionTuple() |
432 self.initpos = event.GetPosition() |
330 else: |
433 else: |
331 p1 = self.initpos |
434 p1 = self.initpos |
332 p2 = event.GetPositionTuple() |
435 p2 = event.GetPosition() |
333 sz = self.GetClientSize() |
436 sz = self.GetClientSize() |
334 p1x = float(p1[0]) / (sz[0] / 2) - 1 |
437 p1x = p1[0] / (sz[0] / 2) - 1 |
335 p1y = 1 - float(p1[1]) / (sz[1] / 2) |
438 p1y = 1 - p1[1] / (sz[1] / 2) |
336 p2x = float(p2[0]) / (sz[0] / 2) - 1 |
439 p2x = p2[0] / (sz[0] / 2) - 1 |
337 p2y = 1 - float(p2[1]) / (sz[1] / 2) |
440 p2y = 1 - p2[1] / (sz[1] / 2) |
338 quat = trackball(p1x, p1y, p2x, p2y, self.dist / 250.0) |
441 quat = trackball(p1x, p1y, p2x, p2y, self.dist / 250.0) |
339 with self.rot_lock: |
442 with self.rot_lock: |
340 self.basequat = mulquat(self.basequat, quat) |
443 if self.orbit_control: |
|
444 self.basequat = self.orbit(p1x, p1y, p2x, p2y) |
|
445 else: |
|
446 self.basequat = mulquat(self.basequat, quat) |
341 self.initpos = p2 |
447 self.initpos = p2 |
342 |
448 |
343 def handle_translation(self, event): |
449 def handle_translation(self, event): |
344 if self.initpos is None: |
450 if self.initpos is None: |
345 self.initpos = event.GetPositionTuple() |
451 self.initpos = event.GetPosition() |
346 else: |
452 else: |
347 p1 = self.initpos |
453 p1 = self.initpos |
348 p2 = event.GetPositionTuple() |
454 p2 = event.GetPosition() |
349 if self.orthographic: |
455 if self.orthographic: |
350 x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) |
456 x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) |
351 x2, y2, _ = self.mouse_to_3d(p2[0], p2[1]) |
457 x2, y2, _ = self.mouse_to_3d(p2[0], p2[1]) |
352 glTranslatef(x2 - x1, y2 - y1, 0) |
458 glTranslatef(x2 - x1, y2 - y1, 0) |
353 else: |
459 else: |