Wed, 20 Jan 2021 10:15:13 +0100
updated and added new files for printrun
15 | 1 | # This file is part of the Printrun suite. |
2 | # | |
3 | # Printrun is free software: you can redistribute it and/or modify | |
4 | # it under the terms of the GNU General Public License as published by | |
5 | # the Free Software Foundation, either version 3 of the License, or | |
6 | # (at your option) any later version. | |
7 | # | |
8 | # Printrun is distributed in the hope that it will be useful, | |
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | # GNU General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU General Public License | |
14 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
16 | from threading import Lock | |
17 | import logging | |
18 | import traceback | |
19 | import numpy | |
20 | import numpy.linalg | |
21 | ||
22 | import wx | |
23 | from wx import glcanvas | |
24 | ||
25 | import pyglet | |
26 | pyglet.options['debug_gl'] = True | |
27 | ||
28 | from pyglet.gl import glEnable, glDisable, GL_LIGHTING, glLightfv, \ | |
29 | GL_LIGHT0, GL_LIGHT1, GL_LIGHT2, GL_POSITION, GL_DIFFUSE, \ | |
30 | GL_AMBIENT, GL_SPECULAR, GL_COLOR_MATERIAL, \ | |
31 | glShadeModel, GL_SMOOTH, GL_NORMALIZE, \ | |
32 | GL_BLEND, glBlendFunc, glClear, glClearColor, \ | |
33 | glClearDepth, GL_COLOR_BUFFER_BIT, GL_CULL_FACE, \ | |
34 | GL_DEPTH_BUFFER_BIT, glDepthFunc, GL_DEPTH_TEST, \ | |
35 | GLdouble, glGetDoublev, glGetIntegerv, GLint, \ | |
36 | GL_LEQUAL, glLoadIdentity, glMatrixMode, GL_MODELVIEW, \ | |
37 | GL_MODELVIEW_MATRIX, GL_ONE_MINUS_SRC_ALPHA, glOrtho, \ | |
38 | GL_PROJECTION, GL_PROJECTION_MATRIX, glScalef, \ | |
39 | GL_SRC_ALPHA, glTranslatef, gluPerspective, gluUnProject, \ | |
46 | 40 | glViewport, GL_VIEWPORT, glPushMatrix, glPopMatrix, \ |
41 | glBegin, glVertex2f, glVertex3f, glEnd, GL_LINE_LOOP, glColor3f, \ | |
42 | GL_LINE_STIPPLE, glColor4f, glLineStipple | |
43 | ||
15 | 44 | from pyglet import gl |
46 | 45 | from .trackball import trackball, mulquat, axis_to_quat |
15 | 46 | from .libtatlin.actors import vec |
46 | 47 | from pyglet.gl.glu import gluOrtho2D |
15 | 48 | |
46 | 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): | |
15 | 58 | '''A simple class for using OpenGL with wxPython.''' |
59 | ||
46 | 60 | orbit_control = True |
15 | 61 | orthographic = True |
62 | color_background = (0.98, 0.98, 0.78, 1) | |
63 | do_lights = True | |
64 | ||
46 | 65 | def __init__(self, parent, pos = wx.DefaultPosition, |
15 | 66 | size = wx.DefaultSize, style = 0, |
67 | antialias_samples = 0): | |
46 | 68 | # Full repaint should not be a performance problem |
69 | #TODO: test on windows, tested in Ubuntu | |
70 | style = style | wx.FULL_REPAINT_ON_RESIZE | |
15 | 71 | |
72 | self.GLinitialized = False | |
73 | self.mview_initialized = False | |
46 | 74 | attribList = [glcanvas.WX_GL_RGBA, # RGBA |
15 | 75 | glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered |
46 | 76 | glcanvas.WX_GL_DEPTH_SIZE, 24 # 24 bit |
77 | ] | |
15 | 78 | |
79 | if antialias_samples > 0 and hasattr(glcanvas, "WX_GL_SAMPLE_BUFFERS"): | |
80 | attribList += (glcanvas.WX_GL_SAMPLE_BUFFERS, 1, | |
81 | glcanvas.WX_GL_SAMPLES, antialias_samples) | |
82 | ||
46 | 83 | attribList.append(0) |
15 | 84 | |
46 | 85 | if BASE_CLASS is glcanvas.GLCanvas: |
86 | super().__init__(parent, wx.ID_ANY, attribList, pos, size, style) | |
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 | ||
15 | 94 | self.context = glcanvas.GLContext(self.canvas) |
95 | ||
96 | self.rot_lock = Lock() | |
97 | self.basequat = [0, 0, 0, 1] | |
98 | self.zoom_factor = 1.0 | |
46 | 99 | self.angle_z = 0 |
100 | self.angle_x = 0 | |
15 | 101 | |
102 | self.gl_broken = False | |
103 | ||
104 | # bind events | |
46 | 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 | ||
15 | 111 | self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent) |
46 | 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 | |
15 | 116 | self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) |
117 | ||
46 | 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) | |
143 | ||
15 | 144 | def processEraseBackgroundEvent(self, event): |
145 | '''Process the erase background event.''' | |
146 | pass # Do nothing, to avoid flashing on MSWin | |
147 | ||
148 | def processSizeEvent(self, event): | |
149 | '''Process the resize event.''' | |
46 | 150 | |
151 | # print('processSizeEvent frozen', self.IsFrozen(), event.Size.x, self.ClientSize.x) | |
152 | if not self.IsFrozen() and self.canvas.IsShownOnScreen(): | |
15 | 153 | # Make sure the frame is shown before calling SetCurrent. |
154 | self.canvas.SetCurrent(self.context) | |
155 | self.OnReshape() | |
46 | 156 | |
157 | # self.Refresh(False) | |
158 | # print('Refresh') | |
15 | 159 | event.Skip() |
160 | ||
161 | def processPaintEvent(self, event): | |
162 | '''Process the drawing event.''' | |
46 | 163 | # print('wxGLPanel.processPaintEvent', self.ClientSize.Width) |
15 | 164 | self.canvas.SetCurrent(self.context) |
165 | ||
166 | if not self.gl_broken: | |
167 | try: | |
168 | self.OnInitGL() | |
46 | 169 | self.DrawCanvas() |
15 | 170 | except pyglet.gl.lib.GLException: |
171 | self.gl_broken = True | |
172 | logging.error(_("OpenGL failed, disabling it:") | |
173 | + "\n" + traceback.format_exc()) | |
174 | event.Skip() | |
175 | ||
176 | def Destroy(self): | |
177 | # clean up the pyglet OpenGL context | |
178 | self.pygletcontext.destroy() | |
179 | # call the super method | |
46 | 180 | super().Destroy() |
15 | 181 | |
182 | # ========================================================================== | |
183 | # GLFrame OpenGL Event Handlers | |
184 | # ========================================================================== | |
185 | def OnInitGL(self, call_reshape = True): | |
186 | '''Initialize OpenGL for use in the window.''' | |
187 | if self.GLinitialized: | |
188 | return | |
189 | self.GLinitialized = True | |
190 | # create a pyglet context for this panel | |
191 | self.pygletcontext = gl.Context(gl.current_context) | |
192 | self.pygletcontext.canvas = self | |
193 | self.pygletcontext.set_current() | |
194 | # normal gl init | |
195 | glClearColor(*self.color_background) | |
196 | glClearDepth(1.0) # set depth value to 1 | |
197 | glDepthFunc(GL_LEQUAL) | |
198 | glEnable(GL_COLOR_MATERIAL) | |
199 | glEnable(GL_DEPTH_TEST) | |
200 | glEnable(GL_CULL_FACE) | |
201 | glEnable(GL_BLEND) | |
202 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) | |
203 | if call_reshape: | |
204 | self.OnReshape() | |
205 | ||
206 | def OnReshape(self): | |
207 | """Reshape the OpenGL viewport based on the size of the window""" | |
208 | size = self.GetClientSize() | |
209 | oldwidth, oldheight = self.width, self.height | |
210 | width, height = size.width, size.height | |
211 | if width < 1 or height < 1: | |
212 | return | |
213 | self.width = max(float(width), 1.0) | |
214 | self.height = max(float(height), 1.0) | |
215 | self.OnInitGL(call_reshape = False) | |
46 | 216 | # print('glViewport', width) |
15 | 217 | glViewport(0, 0, width, height) |
218 | glMatrixMode(GL_PROJECTION) | |
219 | glLoadIdentity() | |
220 | if self.orthographic: | |
221 | glOrtho(-width / 2, width / 2, -height / 2, height / 2, | |
222 | -5 * self.dist, 5 * self.dist) | |
223 | else: | |
224 | gluPerspective(60., float(width) / height, 10.0, 3 * self.dist) | |
225 | glTranslatef(0, 0, -self.dist) # Move back | |
226 | glMatrixMode(GL_MODELVIEW) | |
227 | ||
228 | if not self.mview_initialized: | |
229 | self.reset_mview(0.9) | |
230 | self.mview_initialized = True | |
231 | elif oldwidth is not None and oldheight is not None: | |
232 | wratio = self.width / oldwidth | |
233 | hratio = self.height / oldheight | |
234 | ||
235 | factor = min(wratio * self.zoomed_width, hratio * self.zoomed_height) | |
236 | x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) | |
237 | self.zoom(factor, (x, y)) | |
238 | self.zoomed_width *= wratio / factor | |
239 | self.zoomed_height *= hratio / factor | |
240 | ||
241 | # Wrap text to the width of the window | |
242 | if self.GLinitialized: | |
243 | self.pygletcontext.set_current() | |
244 | self.update_object_resize() | |
245 | ||
246 | def setup_lights(self): | |
247 | if not self.do_lights: | |
248 | return | |
249 | glEnable(GL_LIGHTING) | |
250 | glDisable(GL_LIGHT0) | |
251 | glLightfv(GL_LIGHT0, GL_AMBIENT, vec(0.4, 0.4, 0.4, 1.0)) | |
252 | glLightfv(GL_LIGHT0, GL_SPECULAR, vec(0, 0, 0, 0)) | |
253 | glLightfv(GL_LIGHT0, GL_DIFFUSE, vec(0, 0, 0, 0)) | |
254 | glEnable(GL_LIGHT1) | |
255 | glLightfv(GL_LIGHT1, GL_AMBIENT, vec(0, 0, 0, 1.0)) | |
256 | glLightfv(GL_LIGHT1, GL_SPECULAR, vec(0.6, 0.6, 0.6, 1.0)) | |
257 | glLightfv(GL_LIGHT2, GL_DIFFUSE, vec(0.8, 0.8, 0.8, 1)) | |
258 | glLightfv(GL_LIGHT1, GL_POSITION, vec(1, 2, 3, 0)) | |
259 | glEnable(GL_LIGHT2) | |
260 | glLightfv(GL_LIGHT2, GL_AMBIENT, vec(0, 0, 0, 1.0)) | |
261 | glLightfv(GL_LIGHT2, GL_SPECULAR, vec(0.6, 0.6, 0.6, 1.0)) | |
262 | glLightfv(GL_LIGHT2, GL_DIFFUSE, vec(0.8, 0.8, 0.8, 1)) | |
263 | glLightfv(GL_LIGHT2, GL_POSITION, vec(-1, -1, 3, 0)) | |
264 | glEnable(GL_NORMALIZE) | |
265 | glShadeModel(GL_SMOOTH) | |
266 | ||
267 | def reset_mview(self, factor): | |
268 | glMatrixMode(GL_MODELVIEW) | |
269 | glLoadIdentity() | |
270 | self.setup_lights() | |
271 | if self.orthographic: | |
272 | wratio = self.width / self.dist | |
273 | hratio = self.height / self.dist | |
274 | minratio = float(min(wratio, hratio)) | |
275 | self.zoom_factor = 1.0 | |
276 | self.zoomed_width = wratio / minratio | |
277 | self.zoomed_height = hratio / minratio | |
278 | glScalef(factor * minratio, factor * minratio, 1) | |
279 | ||
46 | 280 | def DrawCanvas(self): |
15 | 281 | """Draw the window.""" |
46 | 282 | #import time |
283 | #start = time.perf_counter() | |
284 | # print('DrawCanvas', self.canvas.GetClientRect()) | |
15 | 285 | self.pygletcontext.set_current() |
286 | glClearColor(*self.color_background) | |
287 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) | |
288 | self.draw_objects() | |
46 | 289 | |
290 | if self.canvas.HasFocus(): | |
291 | self.drawFocus() | |
15 | 292 | self.canvas.SwapBuffers() |
46 | 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() | |
15 | 320 | |
321 | # ========================================================================== | |
322 | # To be implemented by a sub class | |
323 | # ========================================================================== | |
324 | def create_objects(self): | |
325 | '''create opengl objects when opengl is initialized''' | |
326 | pass | |
327 | ||
328 | def update_object_resize(self): | |
329 | '''called when the window recieves only if opengl is initialized''' | |
330 | pass | |
331 | ||
332 | def draw_objects(self): | |
333 | '''called in the middle of ondraw after the buffer has been cleared''' | |
334 | pass | |
335 | ||
336 | # ========================================================================== | |
337 | # Utils | |
338 | # ========================================================================== | |
339 | def get_modelview_mat(self, local_transform): | |
340 | mvmat = (GLdouble * 16)() | |
341 | glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) | |
342 | return mvmat | |
343 | ||
344 | def mouse_to_3d(self, x, y, z = 1.0, local_transform = False): | |
345 | x = float(x) | |
346 | y = self.height - float(y) | |
347 | # The following could work if we were not initially scaling to zoom on | |
348 | # the bed | |
349 | # if self.orthographic: | |
350 | # return (x - self.width / 2, y - self.height / 2, 0) | |
351 | pmat = (GLdouble * 16)() | |
352 | mvmat = self.get_modelview_mat(local_transform) | |
353 | viewport = (GLint * 4)() | |
354 | px = (GLdouble)() | |
355 | py = (GLdouble)() | |
356 | pz = (GLdouble)() | |
357 | glGetIntegerv(GL_VIEWPORT, viewport) | |
358 | glGetDoublev(GL_PROJECTION_MATRIX, pmat) | |
359 | glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) | |
360 | gluUnProject(x, y, z, mvmat, pmat, viewport, px, py, pz) | |
361 | return (px.value, py.value, pz.value) | |
362 | ||
363 | def mouse_to_ray(self, x, y, local_transform = False): | |
364 | x = float(x) | |
365 | y = self.height - float(y) | |
366 | pmat = (GLdouble * 16)() | |
367 | mvmat = (GLdouble * 16)() | |
368 | viewport = (GLint * 4)() | |
369 | px = (GLdouble)() | |
370 | py = (GLdouble)() | |
371 | pz = (GLdouble)() | |
372 | glGetIntegerv(GL_VIEWPORT, viewport) | |
373 | glGetDoublev(GL_PROJECTION_MATRIX, pmat) | |
374 | mvmat = self.get_modelview_mat(local_transform) | |
375 | gluUnProject(x, y, 1, mvmat, pmat, viewport, px, py, pz) | |
376 | ray_far = (px.value, py.value, pz.value) | |
377 | gluUnProject(x, y, 0., mvmat, pmat, viewport, px, py, pz) | |
378 | ray_near = (px.value, py.value, pz.value) | |
379 | return ray_near, ray_far | |
380 | ||
381 | def mouse_to_plane(self, x, y, plane_normal, plane_offset, local_transform = False): | |
382 | # Ray/plane intersection | |
383 | ray_near, ray_far = self.mouse_to_ray(x, y, local_transform) | |
384 | ray_near = numpy.array(ray_near) | |
385 | ray_far = numpy.array(ray_far) | |
386 | ray_dir = ray_far - ray_near | |
387 | ray_dir = ray_dir / numpy.linalg.norm(ray_dir) | |
388 | plane_normal = numpy.array(plane_normal) | |
389 | q = ray_dir.dot(plane_normal) | |
390 | if q == 0: | |
391 | return None | |
392 | t = - (ray_near.dot(plane_normal) + plane_offset) / q | |
393 | if t < 0: | |
394 | return None | |
395 | return ray_near + t * ray_dir | |
396 | ||
397 | def zoom(self, factor, to = None): | |
398 | glMatrixMode(GL_MODELVIEW) | |
399 | if to: | |
400 | delta_x = to[0] | |
401 | delta_y = to[1] | |
402 | glTranslatef(delta_x, delta_y, 0) | |
403 | glScalef(factor, factor, 1) | |
404 | self.zoom_factor *= factor | |
405 | if to: | |
406 | glTranslatef(-delta_x, -delta_y, 0) | |
46 | 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) | |
15 | 414 | |
415 | def zoom_to_center(self, factor): | |
416 | self.canvas.SetCurrent(self.context) | |
417 | x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) | |
418 | self.zoom(factor, (x, y)) | |
419 | ||
46 | 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 | ||
15 | 430 | def handle_rotation(self, event): |
431 | if self.initpos is None: | |
46 | 432 | self.initpos = event.GetPosition() |
15 | 433 | else: |
434 | p1 = self.initpos | |
46 | 435 | p2 = event.GetPosition() |
15 | 436 | sz = self.GetClientSize() |
46 | 437 | p1x = p1[0] / (sz[0] / 2) - 1 |
438 | p1y = 1 - p1[1] / (sz[1] / 2) | |
439 | p2x = p2[0] / (sz[0] / 2) - 1 | |
440 | p2y = 1 - p2[1] / (sz[1] / 2) | |
15 | 441 | quat = trackball(p1x, p1y, p2x, p2y, self.dist / 250.0) |
442 | with self.rot_lock: | |
46 | 443 | if self.orbit_control: |
444 | self.basequat = self.orbit(p1x, p1y, p2x, p2y) | |
445 | else: | |
446 | self.basequat = mulquat(self.basequat, quat) | |
15 | 447 | self.initpos = p2 |
448 | ||
449 | def handle_translation(self, event): | |
450 | if self.initpos is None: | |
46 | 451 | self.initpos = event.GetPosition() |
15 | 452 | else: |
453 | p1 = self.initpos | |
46 | 454 | p2 = event.GetPosition() |
15 | 455 | if self.orthographic: |
456 | x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) | |
457 | x2, y2, _ = self.mouse_to_3d(p2[0], p2[1]) | |
458 | glTranslatef(x2 - x1, y2 - y1, 0) | |
459 | else: | |
460 | glTranslatef(p2[0] - p1[0], -(p2[1] - p1[1]), 0) | |
461 | self.initpos = p2 |