Tue, 19 Jan 2021 20:25:47 +0100
NeoCube laser cutting improvements
15 | 1 | #!/usr/bin/env python |
2 | ||
3 | # This file is part of the Printrun suite. | |
4 | # | |
5 | # Printrun is free software: you can redistribute it and/or modify | |
6 | # it under the terms of the GNU General Public License as published by | |
7 | # the Free Software Foundation, either version 3 of the License, or | |
8 | # (at your option) any later version. | |
9 | # | |
10 | # Printrun is distributed in the hope that it will be useful, | |
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | # GNU General Public License for more details. | |
14 | # | |
15 | # You should have received a copy of the GNU General Public License | |
16 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
17 | ||
18 | from threading import Lock | |
19 | import logging | |
20 | import traceback | |
21 | import numpy | |
22 | import numpy.linalg | |
23 | ||
24 | import wx | |
25 | from wx import glcanvas | |
26 | ||
27 | import pyglet | |
28 | pyglet.options['debug_gl'] = True | |
29 | ||
30 | from pyglet.gl import glEnable, glDisable, GL_LIGHTING, glLightfv, \ | |
31 | GL_LIGHT0, GL_LIGHT1, GL_LIGHT2, GL_POSITION, GL_DIFFUSE, \ | |
32 | GL_AMBIENT, GL_SPECULAR, GL_COLOR_MATERIAL, \ | |
33 | glShadeModel, GL_SMOOTH, GL_NORMALIZE, \ | |
34 | GL_BLEND, glBlendFunc, glClear, glClearColor, \ | |
35 | glClearDepth, GL_COLOR_BUFFER_BIT, GL_CULL_FACE, \ | |
36 | GL_DEPTH_BUFFER_BIT, glDepthFunc, GL_DEPTH_TEST, \ | |
37 | GLdouble, glGetDoublev, glGetIntegerv, GLint, \ | |
38 | GL_LEQUAL, glLoadIdentity, glMatrixMode, GL_MODELVIEW, \ | |
39 | GL_MODELVIEW_MATRIX, GL_ONE_MINUS_SRC_ALPHA, glOrtho, \ | |
40 | GL_PROJECTION, GL_PROJECTION_MATRIX, glScalef, \ | |
41 | GL_SRC_ALPHA, glTranslatef, gluPerspective, gluUnProject, \ | |
42 | glViewport, GL_VIEWPORT | |
43 | from pyglet import gl | |
44 | from .trackball import trackball, mulquat | |
45 | from .libtatlin.actors import vec | |
46 | ||
47 | class wxGLPanel(wx.Panel): | |
48 | '''A simple class for using OpenGL with wxPython.''' | |
49 | ||
50 | orthographic = True | |
51 | color_background = (0.98, 0.98, 0.78, 1) | |
52 | do_lights = True | |
53 | ||
54 | def __init__(self, parent, id, pos = wx.DefaultPosition, | |
55 | size = wx.DefaultSize, style = 0, | |
56 | antialias_samples = 0): | |
57 | # Forcing a no full repaint to stop flickering | |
58 | style = style | wx.NO_FULL_REPAINT_ON_RESIZE | |
59 | super(wxGLPanel, self).__init__(parent, id, pos, size, style) | |
60 | ||
61 | self.GLinitialized = False | |
62 | self.mview_initialized = False | |
63 | attribList = (glcanvas.WX_GL_RGBA, # RGBA | |
64 | glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered | |
65 | glcanvas.WX_GL_DEPTH_SIZE, 24) # 24 bit | |
66 | ||
67 | if antialias_samples > 0 and hasattr(glcanvas, "WX_GL_SAMPLE_BUFFERS"): | |
68 | attribList += (glcanvas.WX_GL_SAMPLE_BUFFERS, 1, | |
69 | glcanvas.WX_GL_SAMPLES, antialias_samples) | |
70 | ||
71 | self.width = None | |
72 | self.height = None | |
73 | ||
74 | self.sizer = wx.BoxSizer(wx.HORIZONTAL) | |
75 | self.canvas = glcanvas.GLCanvas(self, attribList = attribList) | |
76 | self.context = glcanvas.GLContext(self.canvas) | |
77 | self.sizer.Add(self.canvas, 1, wx.EXPAND) | |
78 | self.SetSizerAndFit(self.sizer) | |
79 | ||
80 | self.rot_lock = Lock() | |
81 | self.basequat = [0, 0, 0, 1] | |
82 | self.zoom_factor = 1.0 | |
83 | ||
84 | self.gl_broken = False | |
85 | ||
86 | # bind events | |
87 | self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent) | |
88 | self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) | |
89 | self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) | |
90 | ||
91 | def processEraseBackgroundEvent(self, event): | |
92 | '''Process the erase background event.''' | |
93 | pass # Do nothing, to avoid flashing on MSWin | |
94 | ||
95 | def processSizeEvent(self, event): | |
96 | '''Process the resize event.''' | |
97 | if self.IsFrozen(): | |
98 | event.Skip() | |
99 | return | |
100 | if (wx.VERSION > (2, 9) and self.canvas.IsShownOnScreen()) or self.canvas.GetContext(): | |
101 | # Make sure the frame is shown before calling SetCurrent. | |
102 | self.canvas.SetCurrent(self.context) | |
103 | self.OnReshape() | |
104 | self.Refresh(False) | |
105 | timer = wx.CallLater(100, self.Refresh) | |
106 | timer.Start() | |
107 | event.Skip() | |
108 | ||
109 | def processPaintEvent(self, event): | |
110 | '''Process the drawing event.''' | |
111 | self.canvas.SetCurrent(self.context) | |
112 | ||
113 | if not self.gl_broken: | |
114 | try: | |
115 | self.OnInitGL() | |
116 | self.OnDraw() | |
117 | except pyglet.gl.lib.GLException: | |
118 | self.gl_broken = True | |
119 | logging.error(_("OpenGL failed, disabling it:") | |
120 | + "\n" + traceback.format_exc()) | |
121 | event.Skip() | |
122 | ||
123 | def Destroy(self): | |
124 | # clean up the pyglet OpenGL context | |
125 | self.pygletcontext.destroy() | |
126 | # call the super method | |
127 | super(wxGLPanel, self).Destroy() | |
128 | ||
129 | # ========================================================================== | |
130 | # GLFrame OpenGL Event Handlers | |
131 | # ========================================================================== | |
132 | def OnInitGL(self, call_reshape = True): | |
133 | '''Initialize OpenGL for use in the window.''' | |
134 | if self.GLinitialized: | |
135 | return | |
136 | self.GLinitialized = True | |
137 | # create a pyglet context for this panel | |
138 | self.pygletcontext = gl.Context(gl.current_context) | |
139 | self.pygletcontext.canvas = self | |
140 | self.pygletcontext.set_current() | |
141 | # normal gl init | |
142 | glClearColor(*self.color_background) | |
143 | glClearDepth(1.0) # set depth value to 1 | |
144 | glDepthFunc(GL_LEQUAL) | |
145 | glEnable(GL_COLOR_MATERIAL) | |
146 | glEnable(GL_DEPTH_TEST) | |
147 | glEnable(GL_CULL_FACE) | |
148 | glEnable(GL_BLEND) | |
149 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) | |
150 | if call_reshape: | |
151 | self.OnReshape() | |
152 | ||
153 | def OnReshape(self): | |
154 | """Reshape the OpenGL viewport based on the size of the window""" | |
155 | size = self.GetClientSize() | |
156 | oldwidth, oldheight = self.width, self.height | |
157 | width, height = size.width, size.height | |
158 | if width < 1 or height < 1: | |
159 | return | |
160 | self.width = max(float(width), 1.0) | |
161 | self.height = max(float(height), 1.0) | |
162 | self.OnInitGL(call_reshape = False) | |
163 | glViewport(0, 0, width, height) | |
164 | glMatrixMode(GL_PROJECTION) | |
165 | glLoadIdentity() | |
166 | if self.orthographic: | |
167 | glOrtho(-width / 2, width / 2, -height / 2, height / 2, | |
168 | -5 * self.dist, 5 * self.dist) | |
169 | else: | |
170 | gluPerspective(60., float(width) / height, 10.0, 3 * self.dist) | |
171 | glTranslatef(0, 0, -self.dist) # Move back | |
172 | glMatrixMode(GL_MODELVIEW) | |
173 | ||
174 | if not self.mview_initialized: | |
175 | self.reset_mview(0.9) | |
176 | self.mview_initialized = True | |
177 | elif oldwidth is not None and oldheight is not None: | |
178 | wratio = self.width / oldwidth | |
179 | hratio = self.height / oldheight | |
180 | ||
181 | factor = min(wratio * self.zoomed_width, hratio * self.zoomed_height) | |
182 | x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) | |
183 | self.zoom(factor, (x, y)) | |
184 | self.zoomed_width *= wratio / factor | |
185 | self.zoomed_height *= hratio / factor | |
186 | ||
187 | # Wrap text to the width of the window | |
188 | if self.GLinitialized: | |
189 | self.pygletcontext.set_current() | |
190 | self.update_object_resize() | |
191 | ||
192 | def setup_lights(self): | |
193 | if not self.do_lights: | |
194 | return | |
195 | glEnable(GL_LIGHTING) | |
196 | glDisable(GL_LIGHT0) | |
197 | glLightfv(GL_LIGHT0, GL_AMBIENT, vec(0.4, 0.4, 0.4, 1.0)) | |
198 | glLightfv(GL_LIGHT0, GL_SPECULAR, vec(0, 0, 0, 0)) | |
199 | glLightfv(GL_LIGHT0, GL_DIFFUSE, vec(0, 0, 0, 0)) | |
200 | glEnable(GL_LIGHT1) | |
201 | glLightfv(GL_LIGHT1, GL_AMBIENT, vec(0, 0, 0, 1.0)) | |
202 | glLightfv(GL_LIGHT1, GL_SPECULAR, vec(0.6, 0.6, 0.6, 1.0)) | |
203 | glLightfv(GL_LIGHT2, GL_DIFFUSE, vec(0.8, 0.8, 0.8, 1)) | |
204 | glLightfv(GL_LIGHT1, GL_POSITION, vec(1, 2, 3, 0)) | |
205 | glEnable(GL_LIGHT2) | |
206 | glLightfv(GL_LIGHT2, GL_AMBIENT, vec(0, 0, 0, 1.0)) | |
207 | glLightfv(GL_LIGHT2, GL_SPECULAR, vec(0.6, 0.6, 0.6, 1.0)) | |
208 | glLightfv(GL_LIGHT2, GL_DIFFUSE, vec(0.8, 0.8, 0.8, 1)) | |
209 | glLightfv(GL_LIGHT2, GL_POSITION, vec(-1, -1, 3, 0)) | |
210 | glEnable(GL_NORMALIZE) | |
211 | glShadeModel(GL_SMOOTH) | |
212 | ||
213 | def reset_mview(self, factor): | |
214 | glMatrixMode(GL_MODELVIEW) | |
215 | glLoadIdentity() | |
216 | self.setup_lights() | |
217 | if self.orthographic: | |
218 | wratio = self.width / self.dist | |
219 | hratio = self.height / self.dist | |
220 | minratio = float(min(wratio, hratio)) | |
221 | self.zoom_factor = 1.0 | |
222 | self.zoomed_width = wratio / minratio | |
223 | self.zoomed_height = hratio / minratio | |
224 | glScalef(factor * minratio, factor * minratio, 1) | |
225 | ||
226 | def OnDraw(self, *args, **kwargs): | |
227 | """Draw the window.""" | |
228 | self.pygletcontext.set_current() | |
229 | glClearColor(*self.color_background) | |
230 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) | |
231 | self.draw_objects() | |
232 | self.canvas.SwapBuffers() | |
233 | ||
234 | # ========================================================================== | |
235 | # To be implemented by a sub class | |
236 | # ========================================================================== | |
237 | def create_objects(self): | |
238 | '''create opengl objects when opengl is initialized''' | |
239 | pass | |
240 | ||
241 | def update_object_resize(self): | |
242 | '''called when the window recieves only if opengl is initialized''' | |
243 | pass | |
244 | ||
245 | def draw_objects(self): | |
246 | '''called in the middle of ondraw after the buffer has been cleared''' | |
247 | pass | |
248 | ||
249 | # ========================================================================== | |
250 | # Utils | |
251 | # ========================================================================== | |
252 | def get_modelview_mat(self, local_transform): | |
253 | mvmat = (GLdouble * 16)() | |
254 | glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) | |
255 | return mvmat | |
256 | ||
257 | def mouse_to_3d(self, x, y, z = 1.0, local_transform = False): | |
258 | x = float(x) | |
259 | y = self.height - float(y) | |
260 | # The following could work if we were not initially scaling to zoom on | |
261 | # the bed | |
262 | # if self.orthographic: | |
263 | # return (x - self.width / 2, y - self.height / 2, 0) | |
264 | pmat = (GLdouble * 16)() | |
265 | mvmat = self.get_modelview_mat(local_transform) | |
266 | viewport = (GLint * 4)() | |
267 | px = (GLdouble)() | |
268 | py = (GLdouble)() | |
269 | pz = (GLdouble)() | |
270 | glGetIntegerv(GL_VIEWPORT, viewport) | |
271 | glGetDoublev(GL_PROJECTION_MATRIX, pmat) | |
272 | glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) | |
273 | gluUnProject(x, y, z, mvmat, pmat, viewport, px, py, pz) | |
274 | return (px.value, py.value, pz.value) | |
275 | ||
276 | def mouse_to_ray(self, x, y, local_transform = False): | |
277 | x = float(x) | |
278 | y = self.height - float(y) | |
279 | pmat = (GLdouble * 16)() | |
280 | mvmat = (GLdouble * 16)() | |
281 | viewport = (GLint * 4)() | |
282 | px = (GLdouble)() | |
283 | py = (GLdouble)() | |
284 | pz = (GLdouble)() | |
285 | glGetIntegerv(GL_VIEWPORT, viewport) | |
286 | glGetDoublev(GL_PROJECTION_MATRIX, pmat) | |
287 | mvmat = self.get_modelview_mat(local_transform) | |
288 | gluUnProject(x, y, 1, mvmat, pmat, viewport, px, py, pz) | |
289 | ray_far = (px.value, py.value, pz.value) | |
290 | gluUnProject(x, y, 0., mvmat, pmat, viewport, px, py, pz) | |
291 | ray_near = (px.value, py.value, pz.value) | |
292 | return ray_near, ray_far | |
293 | ||
294 | def mouse_to_plane(self, x, y, plane_normal, plane_offset, local_transform = False): | |
295 | # Ray/plane intersection | |
296 | ray_near, ray_far = self.mouse_to_ray(x, y, local_transform) | |
297 | ray_near = numpy.array(ray_near) | |
298 | ray_far = numpy.array(ray_far) | |
299 | ray_dir = ray_far - ray_near | |
300 | ray_dir = ray_dir / numpy.linalg.norm(ray_dir) | |
301 | plane_normal = numpy.array(plane_normal) | |
302 | q = ray_dir.dot(plane_normal) | |
303 | if q == 0: | |
304 | return None | |
305 | t = - (ray_near.dot(plane_normal) + plane_offset) / q | |
306 | if t < 0: | |
307 | return None | |
308 | return ray_near + t * ray_dir | |
309 | ||
310 | def zoom(self, factor, to = None): | |
311 | glMatrixMode(GL_MODELVIEW) | |
312 | if to: | |
313 | delta_x = to[0] | |
314 | delta_y = to[1] | |
315 | glTranslatef(delta_x, delta_y, 0) | |
316 | glScalef(factor, factor, 1) | |
317 | self.zoom_factor *= factor | |
318 | if to: | |
319 | glTranslatef(-delta_x, -delta_y, 0) | |
320 | wx.CallAfter(self.Refresh) | |
321 | ||
322 | def zoom_to_center(self, factor): | |
323 | self.canvas.SetCurrent(self.context) | |
324 | x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) | |
325 | self.zoom(factor, (x, y)) | |
326 | ||
327 | def handle_rotation(self, event): | |
328 | if self.initpos is None: | |
329 | self.initpos = event.GetPositionTuple() | |
330 | else: | |
331 | p1 = self.initpos | |
332 | p2 = event.GetPositionTuple() | |
333 | sz = self.GetClientSize() | |
334 | p1x = float(p1[0]) / (sz[0] / 2) - 1 | |
335 | p1y = 1 - float(p1[1]) / (sz[1] / 2) | |
336 | p2x = float(p2[0]) / (sz[0] / 2) - 1 | |
337 | p2y = 1 - float(p2[1]) / (sz[1] / 2) | |
338 | quat = trackball(p1x, p1y, p2x, p2y, self.dist / 250.0) | |
339 | with self.rot_lock: | |
340 | self.basequat = mulquat(self.basequat, quat) | |
341 | self.initpos = p2 | |
342 | ||
343 | def handle_translation(self, event): | |
344 | if self.initpos is None: | |
345 | self.initpos = event.GetPositionTuple() | |
346 | else: | |
347 | p1 = self.initpos | |
348 | p2 = event.GetPositionTuple() | |
349 | if self.orthographic: | |
350 | x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) | |
351 | x2, y2, _ = self.mouse_to_3d(p2[0], p2[1]) | |
352 | glTranslatef(x2 - x1, y2 - y1, 0) | |
353 | else: | |
354 | glTranslatef(p2[0] - p1[0], -(p2[1] - p1[1]), 0) | |
355 | self.initpos = p2 |