printrun-src/printrun/stlplater.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
equal deleted inserted replaced
14:51bf56ba3c10 15:0bbb006204fc
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 os
19
20 # Set up Internationalization using gettext
21 # searching for installed locales on /usr/share; uses relative folder if not found (windows)
22 from .utils import install_locale
23 install_locale('pronterface')
24
25 import wx
26 import time
27 import logging
28 import threading
29 import math
30 import sys
31 import re
32 import traceback
33 import subprocess
34 from copy import copy
35
36 from printrun import stltool
37 from printrun.objectplater import make_plater, PlaterPanel
38
39 glview = False
40 if "-nogl" not in sys.argv:
41 try:
42 from printrun import stlview
43 glview = True
44 except:
45 logging.warning("Could not load 3D viewer for plater:"
46 + "\n" + traceback.format_exc())
47
48
49 def evalme(s):
50 return eval(s[s.find("(") + 1:s.find(")")])
51
52 def transformation_matrix(model):
53 matrix = stltool.I
54 if any(model.centeroffset):
55 matrix = model.translation_matrix(model.centeroffset).dot(matrix)
56 if model.rot:
57 matrix = model.rotation_matrix([0, 0, model.rot]).dot(matrix)
58 if any(model.offsets):
59 matrix = model.translation_matrix(model.offsets).dot(matrix)
60 return matrix
61
62 class showstl(wx.Window):
63 def __init__(self, parent, size, pos):
64 wx.Window.__init__(self, parent, size = size, pos = pos)
65 self.i = 0
66 self.parent = parent
67 self.previ = 0
68 self.Bind(wx.EVT_MOUSEWHEEL, self.rot)
69 self.Bind(wx.EVT_MOUSE_EVENTS, self.move)
70 self.Bind(wx.EVT_PAINT, self.repaint)
71 self.Bind(wx.EVT_KEY_DOWN, self.keypress)
72 self.triggered = 0
73 self.initpos = None
74 self.prevsel = -1
75
76 def prepare_model(self, m, scale):
77 m.bitmap = wx.EmptyBitmap(800, 800, 32)
78 dc = wx.MemoryDC()
79 dc.SelectObject(m.bitmap)
80 dc.SetBackground(wx.Brush((0, 0, 0, 0)))
81 dc.SetBrush(wx.Brush((0, 0, 0, 255)))
82 dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128)))
83 dc.SetPen(wx.Pen(wx.Colour(128, 128, 128)))
84 for i in m.facets:
85 dc.DrawPolygon([wx.Point(400 + scale * p[0], (400 - scale * p[1])) for p in i[1]])
86 dc.SelectObject(wx.NullBitmap)
87 m.bitmap.SetMask(wx.Mask(m.bitmap, wx.Colour(0, 0, 0, 255)))
88
89 def move_shape(self, delta):
90 """moves shape (selected in l, which is list ListBox of shapes)
91 by an offset specified in tuple delta.
92 Positive numbers move to (rigt, down)"""
93 name = self.parent.l.GetSelection()
94 if name == wx.NOT_FOUND:
95 return False
96 name = self.parent.l.GetString(name)
97 model = self.parent.models[name]
98 model.offsets = [model.offsets[0] + delta[0],
99 model.offsets[1] + delta[1],
100 model.offsets[2]
101 ]
102 self.Refresh()
103 return True
104
105 def move(self, event):
106 if event.ButtonUp(wx.MOUSE_BTN_LEFT):
107 if self.initpos is not None:
108 currentpos = event.GetPositionTuple()
109 delta = (0.5 * (currentpos[0] - self.initpos[0]),
110 -0.5 * (currentpos[1] - self.initpos[1])
111 )
112 self.move_shape(delta)
113 self.Refresh()
114 self.initpos = None
115 elif event.ButtonDown(wx.MOUSE_BTN_RIGHT):
116 self.parent.right(event)
117 elif event.Dragging():
118 if self.initpos is None:
119 self.initpos = event.GetPositionTuple()
120 self.Refresh()
121 dc = wx.ClientDC(self)
122 p = event.GetPositionTuple()
123 dc.DrawLine(self.initpos[0], self.initpos[1], p[0], p[1])
124 del dc
125 else:
126 event.Skip()
127
128 def rotate_shape(self, angle):
129 """rotates acive shape
130 positive angle is clockwise
131 """
132 self.i += angle
133 if not self.triggered:
134 self.triggered = 1
135 threading.Thread(target = self.cr).start()
136
137 def keypress(self, event):
138 """gets keypress events and moves/rotates acive shape"""
139 keycode = event.GetKeyCode()
140 step = 5
141 angle = 18
142 if event.ControlDown():
143 step = 1
144 angle = 1
145 # h
146 if keycode == 72:
147 self.move_shape((-step, 0))
148 # l
149 if keycode == 76:
150 self.move_shape((step, 0))
151 # j
152 if keycode == 75:
153 self.move_shape((0, step))
154 # k
155 if keycode == 74:
156 self.move_shape((0, -step))
157 # [
158 if keycode == 91:
159 self.rotate_shape(-angle)
160 # ]
161 if keycode == 93:
162 self.rotate_shape(angle)
163 event.Skip()
164
165 def rotateafter(self):
166 if self.i != self.previ:
167 i = self.parent.l.GetSelection()
168 if i != wx.NOT_FOUND:
169 self.parent.models[self.parent.l.GetString(i)].rot -= 5 * (self.i - self.previ)
170 self.previ = self.i
171 self.Refresh()
172
173 def cr(self):
174 time.sleep(0.01)
175 wx.CallAfter(self.rotateafter)
176 self.triggered = 0
177
178 def rot(self, event):
179 z = event.GetWheelRotation()
180 s = self.parent.l.GetSelection()
181 if self.prevsel != s:
182 self.i = 0
183 self.prevsel = s
184 if z < 0:
185 self.rotate_shape(-1)
186 else:
187 self.rotate_shape(1)
188
189 def repaint(self, event):
190 dc = wx.PaintDC(self)
191 self.paint(dc = dc)
192
193 def paint(self, coord1 = "x", coord2 = "y", dc = None):
194 if dc is None:
195 dc = wx.ClientDC(self)
196 scale = 2
197 dc.SetPen(wx.Pen(wx.Colour(100, 100, 100)))
198 for i in xrange(20):
199 dc.DrawLine(0, i * scale * 10, 400, i * scale * 10)
200 dc.DrawLine(i * scale * 10, 0, i * scale * 10, 400)
201 dc.SetPen(wx.Pen(wx.Colour(0, 0, 0)))
202 for i in xrange(4):
203 dc.DrawLine(0, i * scale * 50, 400, i * scale * 50)
204 dc.DrawLine(i * scale * 50, 0, i * scale * 50, 400)
205 dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128)))
206 dc.SetPen(wx.Pen(wx.Colour(128, 128, 128)))
207 dcs = wx.MemoryDC()
208 for m in self.parent.models.values():
209 b = m.bitmap
210 im = b.ConvertToImage()
211 imgc = wx.Point(im.GetWidth() / 2, im.GetHeight() / 2)
212 im = im.Rotate(math.radians(m.rot), imgc, 0)
213 bm = wx.BitmapFromImage(im)
214 dcs.SelectObject(bm)
215 bsz = bm.GetSize()
216 dc.Blit(scale * m.offsets[0] - bsz[0] / 2, 400 - (scale * m.offsets[1] + bsz[1] / 2), bsz[0], bsz[1], dcs, 0, 0, useMask = 1)
217 del dc
218
219 class StlPlaterPanel(PlaterPanel):
220
221 load_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL|OpenSCAD files (*.scad)|*.scad")
222 save_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL")
223
224 def prepare_ui(self, filenames = [], callback = None,
225 parent = None, build_dimensions = None, circular_platform = False,
226 simarrange_path = None, antialias_samples = 0):
227 super(StlPlaterPanel, self).prepare_ui(filenames, callback, parent, build_dimensions)
228 self.cutting = False
229 self.cutting_axis = None
230 self.cutting_dist = None
231 if glview:
232 viewer = stlview.StlViewPanel(self, (580, 580),
233 build_dimensions = self.build_dimensions,
234 circular = circular_platform,
235 antialias_samples = antialias_samples)
236 # Cutting tool
237 nrows = self.menusizer.GetRows()
238 self.menusizer.Add(wx.StaticText(self.menupanel, -1, _("Cut along:")),
239 pos = (nrows, 0), span = (1, 1), flag = wx.ALIGN_CENTER)
240 cutconfirmbutton = wx.Button(self.menupanel, label = _("Confirm cut"))
241 cutconfirmbutton.Bind(wx.EVT_BUTTON, self.cut_confirm)
242 cutconfirmbutton.Disable()
243 self.cutconfirmbutton = cutconfirmbutton
244 self.menusizer.Add(cutconfirmbutton, pos = (nrows, 1), span = (1, 1), flag = wx.EXPAND)
245 cutpanel = wx.Panel(self.menupanel, -1)
246 cutsizer = self.cutsizer = wx.BoxSizer(wx.HORIZONTAL)
247 cutpanel.SetSizer(cutsizer)
248 cutxplusbutton = wx.ToggleButton(cutpanel, label = _(">X"), style = wx.BU_EXACTFIT)
249 cutxplusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "x", 1))
250 cutsizer.Add(cutxplusbutton, 1, flag = wx.EXPAND)
251 cutzplusbutton = wx.ToggleButton(cutpanel, label = _(">Y"), style = wx.BU_EXACTFIT)
252 cutzplusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "y", 1))
253 cutsizer.Add(cutzplusbutton, 1, flag = wx.EXPAND)
254 cutzplusbutton = wx.ToggleButton(cutpanel, label = _(">Z"), style = wx.BU_EXACTFIT)
255 cutzplusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "z", 1))
256 cutsizer.Add(cutzplusbutton, 1, flag = wx.EXPAND)
257 cutxminusbutton = wx.ToggleButton(cutpanel, label = _("<X"), style = wx.BU_EXACTFIT)
258 cutxminusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "x", -1))
259 cutsizer.Add(cutxminusbutton, 1, flag = wx.EXPAND)
260 cutzminusbutton = wx.ToggleButton(cutpanel, label = _("<Y"), style = wx.BU_EXACTFIT)
261 cutzminusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "y", -1))
262 cutsizer.Add(cutzminusbutton, 1, flag = wx.EXPAND)
263 cutzminusbutton = wx.ToggleButton(cutpanel, label = _("<Z"), style = wx.BU_EXACTFIT)
264 cutzminusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "z", -1))
265 cutsizer.Add(cutzminusbutton, 1, flag = wx.EXPAND)
266 self.menusizer.Add(cutpanel, pos = (nrows + 1, 0), span = (1, 2), flag = wx.EXPAND)
267 else:
268 viewer = showstl(self, (580, 580), (0, 0))
269 self.simarrange_path = simarrange_path
270 self.set_viewer(viewer)
271
272 def start_cutting_tool(self, event, axis, direction):
273 toggle = event.GetEventObject()
274 if toggle.GetValue():
275 # Disable the other toggles
276 for child in self.cutsizer.GetChildren():
277 child = child.GetWindow()
278 if child != toggle:
279 child.SetValue(False)
280 self.cutting = True
281 self.cutting_axis = axis
282 self.cutting_dist = None
283 self.cutting_direction = direction
284 else:
285 self.cutting = False
286 self.cutting_axis = None
287 self.cutting_dist = None
288 self.cutting_direction = None
289
290 def cut_confirm(self, event):
291 name = self.l.GetSelection()
292 name = self.l.GetString(name)
293 model = self.models[name]
294 transformation = transformation_matrix(model)
295 transformed = model.transform(transformation)
296 logging.info(_("Cutting %s alongside %s axis") % (name, self.cutting_axis))
297 axes = ["x", "y", "z"]
298 cut = transformed.cut(axes.index(self.cutting_axis),
299 self.cutting_direction,
300 self.cutting_dist)
301 cut.offsets = [0, 0, 0]
302 cut.rot = 0
303 cut.scale = model.scale
304 cut.filename = model.filename
305 cut.centeroffset = [0, 0, 0]
306 self.s.prepare_model(cut, 2)
307 self.models[name] = cut
308 self.cutconfirmbutton.Disable()
309 self.cutting = False
310 self.cutting_axis = None
311 self.cutting_dist = None
312 self.cutting_direction = None
313 for child in self.cutsizer.GetChildren():
314 child = child.GetWindow()
315 child.SetValue(False)
316
317 def clickcb(self, event, single = False):
318 if not isinstance(self.s, stlview.StlViewPanel):
319 return
320 if self.cutting:
321 self.clickcb_cut(event)
322 else:
323 self.clickcb_rebase(event)
324
325 def clickcb_cut(self, event):
326 axis = self.cutting_axis
327 self.cutting_dist, _, _ = self.s.get_cutting_plane(axis, None,
328 local_transform = True)
329 if self.cutting_dist is not None:
330 self.cutconfirmbutton.Enable()
331
332 def clickcb_rebase(self, event):
333 x, y = event.GetPosition()
334 ray_near, ray_far = self.s.mouse_to_ray(x, y, local_transform = True)
335 best_match = None
336 best_facet = None
337 best_dist = float("inf")
338 for key, model in self.models.iteritems():
339 transformation = transformation_matrix(model)
340 transformed = model.transform(transformation)
341 if not transformed.intersect_box(ray_near, ray_far):
342 logging.debug("Skipping %s for rebase search" % key)
343 continue
344 facet, facet_dist = transformed.intersect(ray_near, ray_far)
345 if facet is not None and facet_dist < best_dist:
346 best_match = key
347 best_facet = facet
348 best_dist = facet_dist
349 if best_match is not None:
350 logging.info("Rebasing %s" % best_match)
351 model = self.models[best_match]
352 newmodel = model.rebase(best_facet)
353 newmodel.offsets = list(model.offsets)
354 newmodel.rot = 0
355 newmodel.scale = model.scale
356 newmodel.filename = model.filename
357 newmodel.centeroffset = [-(newmodel.dims[1] + newmodel.dims[0]) / 2,
358 -(newmodel.dims[3] + newmodel.dims[2]) / 2,
359 0]
360 self.s.prepare_model(newmodel, 2)
361 self.models[best_match] = newmodel
362 wx.CallAfter(self.Refresh)
363
364 def done(self, event, cb):
365 if not os.path.exists("tempstl"):
366 os.mkdir("tempstl")
367 name = "tempstl/" + str(int(time.time()) % 10000) + ".stl"
368 self.export_to(name)
369 if cb is not None:
370 cb(name)
371 if self.destroy_on_done:
372 self.Destroy()
373
374 def load_file(self, filename):
375 if filename.lower().endswith(".stl"):
376 try:
377 self.load_stl(filename)
378 except:
379 dlg = wx.MessageDialog(self, _("Loading STL file failed"),
380 _("Error:") + traceback.format_exc(),
381 wx.OK)
382 dlg.ShowModal()
383 logging.error(_("Loading STL file failed:")
384 + "\n" + traceback.format_exc())
385 elif filename.lower().endswith(".scad"):
386 try:
387 self.load_scad(filename)
388 except:
389 dlg = wx.MessageDialog(self, _("Loading OpenSCAD file failed"),
390 _("Error:") + traceback.format_exc(),
391 wx.OK)
392 dlg.ShowModal()
393 logging.error(_("Loading OpenSCAD file failed:")
394 + "\n" + traceback.format_exc())
395
396 def load_scad(self, name):
397 lf = open(name)
398 s = [i.replace("\n", "").replace("\r", "").replace(";", "") for i in lf if "stl" in i]
399 lf.close()
400
401 for i in s:
402 parts = i.split()
403 for part in parts:
404 if 'translate' in part:
405 translate_list = evalme(part)
406 for part in parts:
407 if 'rotate' in part:
408 rotate_list = evalme(part)
409 for part in parts:
410 if 'import' in part:
411 stl_file = evalme(part)
412
413 newname = os.path.split(stl_file.lower())[1]
414 c = 1
415 while newname in self.models:
416 newname = os.path.split(stl_file.lower())[1]
417 newname = newname + "(%d)" % c
418 c += 1
419 stl_path = os.path.join(os.path.split(name)[0:len(os.path.split(stl_file)) - 1])
420 stl_full_path = os.path.join(stl_path[0], str(stl_file))
421 self.load_stl_into_model(stl_full_path, stl_file, translate_list, rotate_list[2])
422
423 def load_stl(self, name):
424 if not os.path.exists(name):
425 logging.error(_("Couldn't load non-existing file %s") % name)
426 return
427 path = os.path.split(name)[0]
428 self.basedir = path
429 if name.lower().endswith(".stl"):
430 for model in self.models.values():
431 if model.filename == name:
432 newmodel = copy(model)
433 newmodel.offsets = list(model.offsets)
434 newmodel.rot = model.rot
435 newmodel.scale = list(model.scale)
436 self.add_model(name, newmodel)
437 self.s.prepare_model(newmodel, 2)
438 break
439 else:
440 # Filter out the path, just show the STL filename.
441 self.load_stl_into_model(name, name)
442 wx.CallAfter(self.Refresh)
443
444 def load_stl_into_model(self, path, name, offset = None, rotation = 0, scale = [1.0, 1.0, 1.0]):
445 model = stltool.stl(path)
446 if offset is None:
447 offset = [self.build_dimensions[3], self.build_dimensions[4], 0]
448 model.offsets = list(offset)
449 model.rot = rotation
450 model.scale = list(scale)
451 model.filename = name
452 self.add_model(name, model)
453 model.centeroffset = [-(model.dims[1] + model.dims[0]) / 2,
454 -(model.dims[3] + model.dims[2]) / 2,
455 0]
456 self.s.prepare_model(model, 2)
457
458 def export_to(self, name):
459 with open(name.replace(".", "_") + ".scad", "w") as sf:
460 facets = []
461 for model in self.models.values():
462 r = model.rot
463 o = model.offsets
464 co = model.centeroffset
465 sf.write("translate([%s, %s, %s])"
466 "rotate([0, 0, %s])"
467 "translate([%s, %s, %s])"
468 "import(\"%s\");\n" % (o[0], o[1], o[2],
469 r,
470 co[0], co[1], co[2],
471 model.filename))
472 model = model.transform(transformation_matrix(model))
473 facets += model.facets
474 stltool.emitstl(name, facets, "plater_export")
475 logging.info(_("Wrote plate to %s") % name)
476
477 def autoplate(self, event = None):
478 if self.simarrange_path:
479 try:
480 self.autoplate_simarrange()
481 except Exception, e:
482 logging.warning(_("Failed to use simarrange for plating, "
483 "falling back to the standard method. "
484 "The error was: ") + e)
485 super(StlPlaterPanel, self).autoplate()
486 else:
487 super(StlPlaterPanel, self).autoplate()
488
489 def autoplate_simarrange(self):
490 logging.info(_("Autoplating using simarrange"))
491 models = dict(self.models)
492 files = [model.filename for model in models.values()]
493 command = [self.simarrange_path, "--dryrun",
494 "-m", # Pack around center
495 "-x", str(int(self.build_dimensions[0])),
496 "-y", str(int(self.build_dimensions[1]))] + files
497 p = subprocess.Popen(command, stdout = subprocess.PIPE)
498
499 pos_regexp = re.compile("File: (.*) minx: ([0-9]+), miny: ([0-9]+), minrot: ([0-9]+)")
500 for line in p.stdout:
501 line = line.rstrip()
502 if "Generating plate" in line:
503 plateid = int(line.split()[-1])
504 if plateid > 0:
505 logging.error(_("Plate full, please remove some objects"))
506 break
507 if "File:" in line:
508 bits = pos_regexp.match(line).groups()
509 filename = bits[0]
510 x = float(bits[1])
511 y = float(bits[2])
512 rot = -float(bits[3])
513 for name, model in models.items():
514 # FIXME: not sure this is going to work superwell with utf8
515 if model.filename == filename:
516 model.offsets[0] = x + self.build_dimensions[3]
517 model.offsets[1] = y + self.build_dimensions[4]
518 model.rot = rot
519 del models[name]
520 break
521 if p.wait() != 0:
522 raise RuntimeError(_("simarrange failed"))
523
524 StlPlater = make_plater(StlPlaterPanel)

mercurial