Tue, 19 Jan 2021 20:45:09 +0100
updated main files to new github master version
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 | import wx | |
19 | import time | |
20 | ||
21 | import numpy | |
22 | import pyglet | |
23 | pyglet.options['debug_gl'] = True | |
24 | ||
25 | from pyglet.gl import GL_AMBIENT_AND_DIFFUSE, glBegin, glClearColor, \ | |
26 | glColor3f, GL_CULL_FACE, GL_DEPTH_TEST, GL_DIFFUSE, GL_EMISSION, \ | |
27 | glEnable, glEnd, GL_FILL, GLfloat, GL_FRONT_AND_BACK, GL_LIGHT0, \ | |
28 | GL_LIGHT1, glLightfv, GL_LIGHTING, GL_LINE, glMaterialf, glMaterialfv, \ | |
29 | glMultMatrixd, glNormal3f, glPolygonMode, glPopMatrix, GL_POSITION, \ | |
30 | glPushMatrix, glRotatef, glScalef, glShadeModel, GL_SHININESS, \ | |
31 | GL_SMOOTH, GL_SPECULAR, glTranslatef, GL_TRIANGLES, glVertex3f, \ | |
32 | glGetDoublev, GL_MODELVIEW_MATRIX, GLdouble, glClearDepth, glDepthFunc, \ | |
33 | GL_LEQUAL, GL_BLEND, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, \ | |
34 | GL_LINE_LOOP, glGetFloatv, GL_LINE_WIDTH, glLineWidth, glDisable, \ | |
35 | GL_LINE_SMOOTH | |
36 | from pyglet import gl | |
37 | ||
38 | from .gl.panel import wxGLPanel | |
39 | from .gl.trackball import build_rotmatrix | |
40 | from .gl.libtatlin import actors | |
41 | ||
42 | def vec(*args): | |
43 | return (GLfloat * len(args))(*args) | |
44 | ||
45 | class stlview(object): | |
46 | def __init__(self, facets, batch): | |
47 | # Create the vertex and normal arrays. | |
48 | vertices = [] | |
49 | normals = [] | |
50 | ||
51 | for i in facets: | |
52 | for j in i[1]: | |
53 | vertices.extend(j) | |
54 | normals.extend(i[0]) | |
55 | ||
56 | # Create a list of triangle indices. | |
57 | indices = range(3 * len(facets)) # [[3*i, 3*i+1, 3*i+2] for i in xrange(len(facets))] | |
58 | self.vertex_list = batch.add_indexed(len(vertices) // 3, | |
59 | GL_TRIANGLES, | |
60 | None, # group, | |
61 | indices, | |
62 | ('v3f/static', vertices), | |
63 | ('n3f/static', normals)) | |
64 | ||
65 | def delete(self): | |
66 | self.vertex_list.delete() | |
67 | ||
68 | class StlViewPanel(wxGLPanel): | |
69 | ||
70 | do_lights = False | |
71 | ||
72 | def __init__(self, parent, size, id = wx.ID_ANY, | |
73 | build_dimensions = None, circular = False, | |
74 | antialias_samples = 0): | |
75 | super(StlViewPanel, self).__init__(parent, id, wx.DefaultPosition, size, 0, | |
76 | antialias_samples = antialias_samples) | |
77 | self.batches = [] | |
78 | self.rot = 0 | |
79 | self.canvas.Bind(wx.EVT_MOUSE_EVENTS, self.move) | |
80 | self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.wheel) | |
81 | self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.double_click) | |
82 | self.initialized = True | |
83 | self.parent = parent | |
84 | self.initpos = None | |
85 | if build_dimensions: | |
86 | self.build_dimensions = build_dimensions | |
87 | else: | |
88 | self.build_dimensions = [200, 200, 100, 0, 0, 0] | |
89 | self.platform = actors.Platform(self.build_dimensions, | |
90 | circular = circular) | |
91 | self.dist = max(self.build_dimensions[0], self.build_dimensions[1]) | |
92 | self.basequat = [0, 0, 0, 1] | |
93 | wx.CallAfter(self.forceresize) | |
94 | self.mousepos = (0, 0) | |
95 | ||
96 | def OnReshape(self): | |
97 | self.mview_initialized = False | |
98 | super(StlViewPanel, self).OnReshape() | |
99 | ||
100 | # ========================================================================== | |
101 | # GLFrame OpenGL Event Handlers | |
102 | # ========================================================================== | |
103 | def OnInitGL(self, call_reshape = True): | |
104 | '''Initialize OpenGL for use in the window.''' | |
105 | if self.GLinitialized: | |
106 | return | |
107 | self.GLinitialized = True | |
108 | # create a pyglet context for this panel | |
109 | self.pygletcontext = gl.Context(gl.current_context) | |
110 | self.pygletcontext.canvas = self | |
111 | self.pygletcontext.set_current() | |
112 | # normal gl init | |
113 | glClearColor(0, 0, 0, 1) | |
114 | glColor3f(1, 0, 0) | |
115 | glEnable(GL_DEPTH_TEST) | |
116 | glClearDepth(1.0) | |
117 | glDepthFunc(GL_LEQUAL) | |
118 | glEnable(GL_CULL_FACE) | |
119 | glEnable(GL_BLEND) | |
120 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) | |
121 | # Uncomment this line for a wireframe view | |
122 | # glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) | |
123 | ||
124 | # Simple light setup. On Windows GL_LIGHT0 is enabled by default, | |
125 | # but this is not the case on Linux or Mac, so remember to always | |
126 | # include it. | |
127 | glEnable(GL_LIGHTING) | |
128 | glEnable(GL_LIGHT0) | |
129 | glEnable(GL_LIGHT1) | |
130 | ||
131 | glLightfv(GL_LIGHT0, GL_POSITION, vec(.5, .5, 1, 0)) | |
132 | glLightfv(GL_LIGHT0, GL_SPECULAR, vec(.5, .5, 1, 1)) | |
133 | glLightfv(GL_LIGHT0, GL_DIFFUSE, vec(1, 1, 1, 1)) | |
134 | glLightfv(GL_LIGHT1, GL_POSITION, vec(1, 0, .5, 0)) | |
135 | glLightfv(GL_LIGHT1, GL_DIFFUSE, vec(.5, .5, .5, 1)) | |
136 | glLightfv(GL_LIGHT1, GL_SPECULAR, vec(1, 1, 1, 1)) | |
137 | glShadeModel(GL_SMOOTH) | |
138 | ||
139 | glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.5, 0, 0.3, 1)) | |
140 | glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, vec(1, 1, 1, 1)) | |
141 | glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 50) | |
142 | glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, vec(0, 0.1, 0, 0.9)) | |
143 | if call_reshape: | |
144 | self.OnReshape() | |
145 | if hasattr(self.parent, "filenames") and self.parent.filenames: | |
146 | for filename in self.parent.filenames: | |
147 | self.parent.load_file(filename) | |
148 | self.parent.autoplate() | |
149 | if hasattr(self.parent, "loadcb"): | |
150 | self.parent.loadcb() | |
151 | self.parent.filenames = None | |
152 | ||
153 | def double_click(self, event): | |
154 | if hasattr(self.parent, "clickcb") and self.parent.clickcb: | |
155 | self.parent.clickcb(event) | |
156 | ||
157 | def forceresize(self): | |
158 | self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] + 1)) | |
159 | self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] - 1)) | |
160 | self.initialized = False | |
161 | ||
162 | def move(self, event): | |
163 | """react to mouse actions: | |
164 | no mouse: show red mousedrop | |
165 | LMB: move active object, | |
166 | with shift rotate viewport | |
167 | RMB: nothing | |
168 | with shift move viewport | |
169 | """ | |
170 | self.mousepos = event.GetPositionTuple() | |
171 | if event.Dragging() and event.LeftIsDown(): | |
172 | self.handle_rotation(event) | |
173 | elif event.Dragging() and event.RightIsDown(): | |
174 | self.handle_translation(event) | |
175 | elif event.ButtonUp(wx.MOUSE_BTN_LEFT): | |
176 | if self.initpos is not None: | |
177 | self.initpos = None | |
178 | elif event.ButtonUp(wx.MOUSE_BTN_RIGHT): | |
179 | if self.initpos is not None: | |
180 | self.initpos = None | |
181 | else: | |
182 | event.Skip() | |
183 | return | |
184 | event.Skip() | |
185 | wx.CallAfter(self.Refresh) | |
186 | ||
187 | def handle_wheel(self, event): | |
188 | delta = event.GetWheelRotation() | |
189 | factor = 1.05 | |
190 | x, y = event.GetPositionTuple() | |
191 | x, y, _ = self.mouse_to_3d(x, y, local_transform = True) | |
192 | if delta > 0: | |
193 | self.zoom(factor, (x, y)) | |
194 | else: | |
195 | self.zoom(1 / factor, (x, y)) | |
196 | ||
197 | def wheel(self, event): | |
198 | """react to mouse wheel actions: | |
199 | rotate object | |
200 | with shift zoom viewport | |
201 | """ | |
202 | self.handle_wheel(event) | |
203 | wx.CallAfter(self.Refresh) | |
204 | ||
205 | def keypress(self, event): | |
206 | """gets keypress events and moves/rotates acive shape""" | |
207 | keycode = event.GetKeyCode() | |
208 | step = 5 | |
209 | angle = 18 | |
210 | if event.ControlDown(): | |
211 | step = 1 | |
212 | angle = 1 | |
213 | # h | |
214 | if keycode == 72: | |
215 | self.parent.move_shape((-step, 0)) | |
216 | # l | |
217 | if keycode == 76: | |
218 | self.parent.move_shape((step, 0)) | |
219 | # j | |
220 | if keycode == 75: | |
221 | self.parent.move_shape((0, step)) | |
222 | # k | |
223 | if keycode == 74: | |
224 | self.parent.move_shape((0, -step)) | |
225 | # [ | |
226 | if keycode == 91: | |
227 | self.parent.rotate_shape(-angle) | |
228 | # ] | |
229 | if keycode == 93: | |
230 | self.parent.rotate_shape(angle) | |
231 | event.Skip() | |
232 | wx.CallAfter(self.Refresh) | |
233 | ||
234 | def anim(self, obj): | |
235 | g = 50 * 9.8 | |
236 | v = 20 | |
237 | dt = 0.05 | |
238 | basepos = obj.offsets[2] | |
239 | obj.offsets[2] += obj.animoffset | |
240 | while obj.offsets[2] > -1: | |
241 | time.sleep(dt) | |
242 | obj.offsets[2] -= v * dt | |
243 | v += g * dt | |
244 | if obj.offsets[2] < 0: | |
245 | obj.scale[2] *= 1 - 3 * dt | |
246 | # return | |
247 | v = v / 4 | |
248 | while obj.offsets[2] < basepos: | |
249 | time.sleep(dt) | |
250 | obj.offsets[2] += v * dt | |
251 | v -= g * dt | |
252 | obj.scale[2] *= 1 + 5 * dt | |
253 | obj.scale[2] = 1.0 | |
254 | ||
255 | def create_objects(self): | |
256 | '''create opengl objects when opengl is initialized''' | |
257 | if not self.platform.initialized: | |
258 | self.platform.init() | |
259 | self.initialized = 1 | |
260 | wx.CallAfter(self.Refresh) | |
261 | ||
262 | def prepare_model(self, m, scale): | |
263 | batch = pyglet.graphics.Batch() | |
264 | stlview(m.facets, batch = batch) | |
265 | m.batch = batch | |
266 | # m.animoffset = 300 | |
267 | # threading.Thread(target = self.anim, args = (m, )).start() | |
268 | wx.CallAfter(self.Refresh) | |
269 | ||
270 | def update_object_resize(self): | |
271 | '''called when the window recieves only if opengl is initialized''' | |
272 | pass | |
273 | ||
274 | def draw_objects(self): | |
275 | '''called in the middle of ondraw after the buffer has been cleared''' | |
276 | self.create_objects() | |
277 | ||
278 | glPushMatrix() | |
279 | glTranslatef(0, 0, -self.dist) | |
280 | glMultMatrixd(build_rotmatrix(self.basequat)) # Rotate according to trackball | |
281 | glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.2, 0.2, 0.2, 1)) | |
282 | glTranslatef(- self.build_dimensions[3] - self.platform.width / 2, | |
283 | - self.build_dimensions[4] - self.platform.depth / 2, 0) # Move origin to bottom left of platform | |
284 | # Draw platform | |
285 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) | |
286 | glDisable(GL_LIGHTING) | |
287 | self.platform.draw() | |
288 | glEnable(GL_LIGHTING) | |
289 | # Draw mouse | |
290 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) | |
291 | inter = self.mouse_to_plane(self.mousepos[0], self.mousepos[1], | |
292 | plane_normal = (0, 0, 1), plane_offset = 0, | |
293 | local_transform = False) | |
294 | if inter is not None: | |
295 | glPushMatrix() | |
296 | glTranslatef(inter[0], inter[1], inter[2]) | |
297 | glBegin(GL_TRIANGLES) | |
298 | glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(1, 0, 0, 1)) | |
299 | glNormal3f(0, 0, 1) | |
300 | glVertex3f(2, 2, 0) | |
301 | glVertex3f(-2, 2, 0) | |
302 | glVertex3f(-2, -2, 0) | |
303 | glVertex3f(2, -2, 0) | |
304 | glVertex3f(2, 2, 0) | |
305 | glVertex3f(-2, -2, 0) | |
306 | glEnd() | |
307 | glPopMatrix() | |
308 | ||
309 | # Draw objects | |
310 | glDisable(GL_CULL_FACE) | |
311 | glPushMatrix() | |
312 | glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.3, 0.7, 0.5, 1)) | |
313 | for i in self.parent.models: | |
314 | model = self.parent.models[i] | |
315 | glPushMatrix() | |
316 | glTranslatef(*(model.offsets)) | |
317 | glRotatef(model.rot, 0.0, 0.0, 1.0) | |
318 | glTranslatef(*(model.centeroffset)) | |
319 | glScalef(*model.scale) | |
320 | model.batch.draw() | |
321 | glPopMatrix() | |
322 | glPopMatrix() | |
323 | glEnable(GL_CULL_FACE) | |
324 | ||
325 | # Draw cutting plane | |
326 | if self.parent.cutting: | |
327 | # FIXME: make this a proper Actor | |
328 | axis = self.parent.cutting_axis | |
329 | fixed_dist = self.parent.cutting_dist | |
330 | dist, plane_width, plane_height = self.get_cutting_plane(axis, fixed_dist) | |
331 | if dist is not None: | |
332 | glPushMatrix() | |
333 | if axis == "x": | |
334 | glRotatef(90, 0, 1, 0) | |
335 | glRotatef(90, 0, 0, 1) | |
336 | glTranslatef(0, 0, dist) | |
337 | elif axis == "y": | |
338 | glRotatef(90, 1, 0, 0) | |
339 | glTranslatef(0, 0, -dist) | |
340 | elif axis == "z": | |
341 | glTranslatef(0, 0, dist) | |
342 | glDisable(GL_CULL_FACE) | |
343 | glBegin(GL_TRIANGLES) | |
344 | glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0, 0.9, 0.15, 0.3)) | |
345 | glNormal3f(0, 0, self.parent.cutting_direction) | |
346 | glVertex3f(plane_width, plane_height, 0) | |
347 | glVertex3f(0, plane_height, 0) | |
348 | glVertex3f(0, 0, 0) | |
349 | glVertex3f(plane_width, 0, 0) | |
350 | glVertex3f(plane_width, plane_height, 0) | |
351 | glVertex3f(0, 0, 0) | |
352 | glEnd() | |
353 | glEnable(GL_CULL_FACE) | |
354 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) | |
355 | glEnable(GL_LINE_SMOOTH) | |
356 | orig_linewidth = (GLfloat)() | |
357 | glGetFloatv(GL_LINE_WIDTH, orig_linewidth) | |
358 | glLineWidth(4.0) | |
359 | glBegin(GL_LINE_LOOP) | |
360 | glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0, 0.8, 0.15, 1)) | |
361 | glVertex3f(0, 0, 0) | |
362 | glVertex3f(0, plane_height, 0) | |
363 | glVertex3f(plane_width, plane_height, 0) | |
364 | glVertex3f(plane_width, 0, 0) | |
365 | glEnd() | |
366 | glLineWidth(orig_linewidth) | |
367 | glDisable(GL_LINE_SMOOTH) | |
368 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) | |
369 | glPopMatrix() | |
370 | ||
371 | glPopMatrix() | |
372 | ||
373 | # ========================================================================== | |
374 | # Utils | |
375 | # ========================================================================== | |
376 | def get_modelview_mat(self, local_transform): | |
377 | mvmat = (GLdouble * 16)() | |
378 | if local_transform: | |
379 | glPushMatrix() | |
380 | # Rotate according to trackball | |
381 | glTranslatef(0, 0, -self.dist) | |
382 | glMultMatrixd(build_rotmatrix(self.basequat)) # Rotate according to trackball | |
383 | glTranslatef(- self.build_dimensions[3] - self.platform.width / 2, | |
384 | - self.build_dimensions[4] - self.platform.depth / 2, 0) # Move origin to bottom left of platform | |
385 | glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) | |
386 | glPopMatrix() | |
387 | else: | |
388 | glGetDoublev(GL_MODELVIEW_MATRIX, mvmat) | |
389 | return mvmat | |
390 | ||
391 | def get_cutting_plane(self, cutting_axis, fixed_dist, local_transform = False): | |
392 | cutting_plane_sizes = {"x": (self.platform.depth, self.platform.height), | |
393 | "y": (self.platform.width, self.platform.height), | |
394 | "z": (self.platform.width, self.platform.depth)} | |
395 | plane_width, plane_height = cutting_plane_sizes[cutting_axis] | |
396 | if fixed_dist is not None: | |
397 | return fixed_dist, plane_width, plane_height | |
398 | ref_sizes = {"x": self.platform.width, | |
399 | "y": self.platform.depth, | |
400 | "z": self.platform.height, | |
401 | } | |
402 | ref_planes = {"x": (0, 0, 1), | |
403 | "y": (0, 0, 1), | |
404 | "z": (0, 1, 0) | |
405 | } | |
406 | ref_offsets = {"x": 0, | |
407 | "y": 0, | |
408 | "z": - self.platform.depth / 2 | |
409 | } | |
410 | translate_axis = {"x": 0, | |
411 | "y": 1, | |
412 | "z": 2 | |
413 | } | |
414 | fallback_ref_planes = {"x": (0, 1, 0), | |
415 | "y": (1, 0, 0), | |
416 | "z": (1, 0, 0) | |
417 | } | |
418 | fallback_ref_offsets = {"x": - self.platform.height / 2, | |
419 | "y": - self.platform.width / 2, | |
420 | "z": - self.platform.width / 2, | |
421 | } | |
422 | ref_size = ref_sizes[cutting_axis] | |
423 | ref_plane = ref_planes[cutting_axis] | |
424 | ref_offset = ref_offsets[cutting_axis] | |
425 | inter = self.mouse_to_plane(self.mousepos[0], self.mousepos[1], | |
426 | plane_normal = ref_plane, | |
427 | plane_offset = ref_offset, | |
428 | local_transform = local_transform) | |
429 | max_size = max((self.platform.width, | |
430 | self.platform.depth, | |
431 | self.platform.height)) | |
432 | dist = None | |
433 | if inter is not None and numpy.fabs(inter).max() + max_size / 2 < 2 * max_size: | |
434 | dist = inter[translate_axis[cutting_axis]] | |
435 | if dist is None or dist < -0.5 * ref_size or dist > 1.5 * ref_size: | |
436 | ref_plane = fallback_ref_planes[cutting_axis] | |
437 | ref_offset = fallback_ref_offsets[cutting_axis] | |
438 | inter = self.mouse_to_plane(self.mousepos[0], self.mousepos[1], | |
439 | plane_normal = ref_plane, | |
440 | plane_offset = ref_offset, | |
441 | local_transform = False) | |
442 | if inter is not None and numpy.fabs(inter).max() + max_size / 2 < 2 * max_size: | |
443 | dist = inter[translate_axis[cutting_axis]] | |
444 | if dist is not None: | |
445 | dist = min(1.5 * ref_size, max(-0.5 * ref_size, dist)) | |
446 | return dist, plane_width, plane_height | |
447 | ||
448 | def main(): | |
449 | app = wx.App(redirect = False) | |
450 | frame = wx.Frame(None, -1, "GL Window", size = (400, 400)) | |
451 | StlViewPanel(frame) | |
452 | frame.Show(True) | |
453 | app.MainLoop() | |
454 | app.Destroy() | |
455 | ||
456 | if __name__ == "__main__": | |
457 | main() |