chiark / gitweb /
Add option to have the center of the printer at 0,0 for RoStock printers. #310 #266
[cura.git] / Cura / gui / projectPlanner.py
1 from __future__ import absolute_import
2
3 import wx
4 import os
5 import threading
6 import time
7 import re
8 import shutil
9 import ConfigParser
10 import numpy
11
12 from wx import glcanvas
13 try:
14         import OpenGL
15         OpenGL.ERROR_CHECKING = False
16         from OpenGL.GLU import *
17         from OpenGL.GL import *
18         hasOpenGLlibs = True
19 except:
20         print "Failed to find PyOpenGL: http://pyopengl.sourceforge.net/"
21         hasOpenGLlibs = False
22
23 from Cura.gui.util import opengl
24 from Cura.gui.util import toolbarUtil
25 from Cura.gui import configBase
26 from Cura.gui import printWindow
27 from Cura.gui.util import dropTarget
28 from Cura.gui.util import taskbar
29 from Cura.util import validators
30 from Cura.util import profile
31 from Cura.util import util3d
32 from Cura.util import meshLoader
33 from Cura.util import stl
34 from Cura.util import mesh
35 from Cura.util import sliceRun
36 from Cura.util import gcodeInterpreter
37 from Cura.util import exporer
38
39 class Action(object):
40         pass
41
42 class ProjectObject(object):
43         def __init__(self, parent, filename):
44                 super(ProjectObject, self).__init__()
45
46                 self.mesh = meshLoader.loadMesh(filename)
47
48                 self.parent = parent
49                 self.filename = filename
50                 self.scale = 1.0
51                 self.rotate = 0.0
52                 self.flipX = False
53                 self.flipY = False
54                 self.flipZ = False
55                 self.swapXZ = False
56                 self.swapYZ = False
57                 self.extruder = 0
58                 self.profile = None
59                 
60                 self.modelDisplayList = None
61                 self.modelDirty = False
62
63                 self.mesh.getMinimumZ()
64                 
65                 self.centerX = -self.getMinimum()[0] + 5
66                 self.centerY = -self.getMinimum()[1] + 5
67                 
68                 self.updateModelTransform()
69
70                 self.centerX = -self.getMinimum()[0] + 5
71                 self.centerY = -self.getMinimum()[1] + 5
72
73         def isSameExceptForPosition(self, other):
74                 if self.filename != other.filename:
75                         return False
76                 if self.scale != other.scale:
77                         return False
78                 if self.rotate != other.rotate:
79                         return False
80                 if self.flipX != other.flipX:
81                         return False
82                 if self.flipY != other.flipY:
83                         return False
84                 if self.flipZ != other.flipZ:
85                         return False
86                 if self.swapXZ != other.swapXZ:
87                         return False
88                 if self.swapYZ != other.swapYZ:
89                         return False
90                 if self.extruder != other.extruder:
91                         return False
92                 if self.profile != other.profile:
93                         return False
94                 return True
95
96         def updateModelTransform(self):
97                 self.mesh.setRotateMirror(self.rotate, self.flipX, self.flipY, self.flipZ, self.swapXZ, self.swapYZ)
98                 minZ = self.mesh.getMinimumZ()
99                 minV = self.getMinimum()
100                 maxV = self.getMaximum()
101                 self.mesh.vertexes -= numpy.array([minV[0] + (maxV[0] - minV[0]) / 2, minV[1] + (maxV[1] - minV[1]) / 2, minZ])
102                 minZ = self.mesh.getMinimumZ()
103                 self.modelDirty = True
104         
105         def getMinimum(self):
106                 return self.mesh.getMinimum()
107         def getMaximum(self):
108                 return self.mesh.getMaximum()
109         def getSize(self):
110                 return self.mesh.getSize()
111         
112         def clone(self):
113                 p = ProjectObject(self.parent, self.filename)
114
115                 p.centerX = self.centerX + 5
116                 p.centerY = self.centerY + 5
117                 
118                 p.filename = self.filename
119                 p.scale = self.scale
120                 p.rotate = self.rotate
121                 p.flipX = self.flipX
122                 p.flipY = self.flipY
123                 p.flipZ = self.flipZ
124                 p.swapXZ = self.swapXZ
125                 p.swapYZ = self.swapYZ
126                 p.extruder = self.extruder
127                 p.profile = self.profile
128                 
129                 p.updateModelTransform()
130                 
131                 return p
132         
133         def clampXY(self):
134                 if self.centerX < -self.getMinimum()[0] * self.scale + self.parent.extruderOffset[self.extruder][0]:
135                         self.centerX = -self.getMinimum()[0] * self.scale + self.parent.extruderOffset[self.extruder][0]
136                 if self.centerY < -self.getMinimum()[1] * self.scale + self.parent.extruderOffset[self.extruder][1]:
137                         self.centerY = -self.getMinimum()[1] * self.scale + self.parent.extruderOffset[self.extruder][1]
138                 if self.centerX > self.parent.machineSize[0] + self.parent.extruderOffset[self.extruder][0] - self.getMaximum()[0] * self.scale:
139                         self.centerX = self.parent.machineSize[0] + self.parent.extruderOffset[self.extruder][0] - self.getMaximum()[0] * self.scale
140                 if self.centerY > self.parent.machineSize[1] + self.parent.extruderOffset[self.extruder][1] - self.getMaximum()[1] * self.scale:
141                         self.centerY = self.parent.machineSize[1] + self.parent.extruderOffset[self.extruder][1] - self.getMaximum()[1] * self.scale
142
143 class projectPlanner(wx.Frame):
144         "Main user interface window"
145         def __init__(self):
146                 super(projectPlanner, self).__init__(None, title='Cura - Project Planner')
147                 
148                 wx.EVT_CLOSE(self, self.OnClose)
149                 self.panel = wx.Panel(self, -1)
150                 self.SetSizer(wx.BoxSizer(wx.VERTICAL))
151                 self.GetSizer().Add(self.panel, 1, flag=wx.EXPAND)
152
153                 self.SetDropTarget(dropTarget.FileDropTarget(self.OnDropFiles, meshLoader.supportedExtensions()))
154                 
155                 self.list = []
156                 self.selection = None
157                 self.printMode = 0
158                 self.alwaysAutoPlace = profile.getPreference('planner_always_autoplace') == 'True'
159
160                 self.machineSize = numpy.array([profile.getPreferenceFloat('machine_width'), profile.getPreferenceFloat('machine_depth'), profile.getPreferenceFloat('machine_height')])
161                 self.headSizeMin = numpy.array([profile.getPreferenceFloat('extruder_head_size_min_x'), profile.getPreferenceFloat('extruder_head_size_min_y'),0])
162                 self.headSizeMax = numpy.array([profile.getPreferenceFloat('extruder_head_size_max_x'), profile.getPreferenceFloat('extruder_head_size_max_y'),0])
163
164                 self.extruderOffset = [
165                         numpy.array([0,0,0]),
166                         numpy.array([profile.getPreferenceFloat('extruder_offset_x1'), profile.getPreferenceFloat('extruder_offset_y1'), 0]),
167                         numpy.array([profile.getPreferenceFloat('extruder_offset_x2'), profile.getPreferenceFloat('extruder_offset_y2'), 0]),
168                         numpy.array([profile.getPreferenceFloat('extruder_offset_x3'), profile.getPreferenceFloat('extruder_offset_y3'), 0])]
169
170                 self.toolbar = toolbarUtil.Toolbar(self.panel)
171
172                 toolbarUtil.NormalButton(self.toolbar, self.OnLoadProject, 'open.png', 'Open project')
173                 toolbarUtil.NormalButton(self.toolbar, self.OnSaveProject, 'save.png', 'Save project')
174                 self.toolbar.AddSeparator()
175                 group = []
176                 toolbarUtil.RadioButton(self.toolbar, group, 'object-3d-on.png', 'object-3d-off.png', '3D view', callback=self.On3DClick).SetValue(self.alwaysAutoPlace)
177                 toolbarUtil.RadioButton(self.toolbar, group, 'object-top-on.png', 'object-top-off.png', 'Topdown view', callback=self.OnTopClick).SetValue(not self.alwaysAutoPlace)
178                 self.toolbar.AddSeparator()
179                 toolbarUtil.NormalButton(self.toolbar, self.OnPreferences, 'preferences.png', 'Project planner preferences')
180                 self.toolbar.AddSeparator()
181                 toolbarUtil.NormalButton(self.toolbar, self.OnCutMesh, 'cut-mesh.png', 'Cut a plate STL into multiple STL files, and add those files to the project.\nNote: Splitting up plates sometimes takes a few minutes.')
182                 toolbarUtil.NormalButton(self.toolbar, self.OnSaveCombinedSTL, 'save-combination.png', 'Save all the combined STL files into a single STL file as a plate.')
183                 self.toolbar.AddSeparator()
184                 group = []
185                 self.printOneAtATime = toolbarUtil.RadioButton(self.toolbar, group, 'view-normal-on.png', 'view-normal-off.png', 'Print one object at a time', callback=self.OnPrintTypeChange)
186                 self.printAllAtOnce = toolbarUtil.RadioButton(self.toolbar, group, 'all-at-once-on.png', 'all-at-once-off.png', 'Print all the objects at once', callback=self.OnPrintTypeChange)
187                 self.toolbar.AddSeparator()
188                 toolbarUtil.NormalButton(self.toolbar, self.OnQuit, 'exit.png', 'Close project planner')
189                 
190                 self.toolbar.Realize()
191
192                 self.toolbar2 = toolbarUtil.Toolbar(self.panel)
193
194                 toolbarUtil.NormalButton(self.toolbar2, self.OnAddModel, 'object-add.png', 'Add model')
195                 toolbarUtil.NormalButton(self.toolbar2, self.OnRemModel, 'object-remove.png', 'Remove model')
196                 self.toolbar2.AddSeparator()
197                 toolbarUtil.NormalButton(self.toolbar2, self.OnMoveUp, 'move-up.png', 'Move model up in print list')
198                 toolbarUtil.NormalButton(self.toolbar2, self.OnMoveDown, 'move-down.png', 'Move model down in print list')
199                 toolbarUtil.NormalButton(self.toolbar2, self.OnCopy, 'copy.png', 'Make a copy of the current selected object')
200                 toolbarUtil.NormalButton(self.toolbar2, self.OnSetCustomProfile, 'set-profile.png', 'Set a custom profile to be used to prepare a specific object.')
201                 self.toolbar2.AddSeparator()
202                 if not self.alwaysAutoPlace:
203                         toolbarUtil.NormalButton(self.toolbar2, self.OnAutoPlace, 'autoplace.png', 'Automaticly organize the objects on the platform.')
204                 toolbarUtil.NormalButton(self.toolbar2, self.OnSlice, 'slice.png', 'Prepare to project into a gcode file.')
205                 self.toolbar2.Realize()
206
207                 self.toolbar3 = toolbarUtil.Toolbar(self.panel)
208                 self.mirrorX = toolbarUtil.ToggleButton(self.toolbar3, 'flip_x', 'object-mirror-x-on.png', 'object-mirror-x-off.png', 'Mirror X', callback=self.OnMirrorChange)
209                 self.mirrorY = toolbarUtil.ToggleButton(self.toolbar3, 'flip_y', 'object-mirror-y-on.png', 'object-mirror-y-off.png', 'Mirror Y', callback=self.OnMirrorChange)
210                 self.mirrorZ = toolbarUtil.ToggleButton(self.toolbar3, 'flip_z', 'object-mirror-z-on.png', 'object-mirror-z-off.png', 'Mirror Z', callback=self.OnMirrorChange)
211                 self.toolbar3.AddSeparator()
212
213                 # Swap
214                 self.swapXZ = toolbarUtil.ToggleButton(self.toolbar3, 'swap_xz', 'object-swap-xz-on.png', 'object-swap-xz-off.png', 'Swap XZ', callback=self.OnMirrorChange)
215                 self.swapYZ = toolbarUtil.ToggleButton(self.toolbar3, 'swap_yz', 'object-swap-yz-on.png', 'object-swap-yz-off.png', 'Swap YZ', callback=self.OnMirrorChange)
216                 self.toolbar3.Realize()
217                 
218                 sizer = wx.GridBagSizer(2,2)
219                 self.panel.SetSizer(sizer)
220                 self.preview = PreviewGLCanvas(self.panel, self)
221                 self.listbox = wx.ListBox(self.panel, -1, choices=[])
222                 self.addButton = wx.Button(self.panel, -1, "Add")
223                 self.remButton = wx.Button(self.panel, -1, "Remove")
224                 self.sliceButton = wx.Button(self.panel, -1, "Prepare")
225                 if not self.alwaysAutoPlace:
226                         self.autoPlaceButton = wx.Button(self.panel, -1, "Auto Place")
227                 
228                 sizer.Add(self.toolbar, (0,0), span=(1,1), flag=wx.EXPAND|wx.LEFT|wx.RIGHT)
229                 sizer.Add(self.toolbar2, (0,1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.RIGHT)
230                 sizer.Add(self.preview, (1,0), span=(5,1), flag=wx.EXPAND)
231                 sizer.Add(self.listbox, (1,1), span=(1,2), flag=wx.EXPAND)
232                 sizer.Add(self.toolbar3, (2,1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.RIGHT)
233                 sizer.Add(self.addButton, (3,1), span=(1,1))
234                 sizer.Add(self.remButton, (3,2), span=(1,1))
235                 sizer.Add(self.sliceButton, (4,1), span=(1,1))
236                 if not self.alwaysAutoPlace:
237                         sizer.Add(self.autoPlaceButton, (4,2), span=(1,1))
238                 sizer.AddGrowableCol(0)
239                 sizer.AddGrowableRow(1)
240                 
241                 self.addButton.Bind(wx.EVT_BUTTON, self.OnAddModel)
242                 self.remButton.Bind(wx.EVT_BUTTON, self.OnRemModel)
243                 self.sliceButton.Bind(wx.EVT_BUTTON, self.OnSlice)
244                 if not self.alwaysAutoPlace:
245                         self.autoPlaceButton.Bind(wx.EVT_BUTTON, self.OnAutoPlace)
246                 self.listbox.Bind(wx.EVT_LISTBOX, self.OnListSelect)
247
248                 panel = wx.Panel(self.panel, -1)
249                 sizer.Add(panel, (5,1), span=(1,2))
250                 
251                 sizer = wx.GridBagSizer(2,2)
252                 panel.SetSizer(sizer)
253                 
254                 self.scaleCtrl = wx.TextCtrl(panel, -1, '')
255                 self.rotateCtrl = wx.SpinCtrl(panel, -1, '', size=(21*4,21), style=wx.SP_ARROW_KEYS)
256                 self.rotateCtrl.SetRange(0, 360)
257
258                 sizer.Add(wx.StaticText(panel, -1, 'Scale'), (0,0), flag=wx.ALIGN_CENTER_VERTICAL)
259                 sizer.Add(self.scaleCtrl, (0,1), flag=wx.ALIGN_BOTTOM|wx.EXPAND)
260                 sizer.Add(wx.StaticText(panel, -1, 'Rotate'), (1,0), flag=wx.ALIGN_CENTER_VERTICAL)
261                 sizer.Add(self.rotateCtrl, (1,1), flag=wx.ALIGN_BOTTOM|wx.EXPAND)
262
263                 if int(profile.getPreference('extruder_amount')) > 1:
264                         self.extruderCtrl = wx.ComboBox(panel, -1, '1', choices=map(str, range(1, int(profile.getPreference('extruder_amount'))+1)), style=wx.CB_DROPDOWN|wx.CB_READONLY)
265                         sizer.Add(wx.StaticText(panel, -1, 'Extruder'), (2,0), flag=wx.ALIGN_CENTER_VERTICAL)
266                         sizer.Add(self.extruderCtrl, (2,1), flag=wx.ALIGN_BOTTOM|wx.EXPAND)
267                         self.extruderCtrl.Bind(wx.EVT_COMBOBOX, self.OnExtruderChange)
268
269                 self.scaleCtrl.Bind(wx.EVT_TEXT, self.OnScaleChange)
270                 self.rotateCtrl.Bind(wx.EVT_SPINCTRL, self.OnRotateChange)
271
272                 self.SetSize((800,600))
273
274         def OnClose(self, e):
275                 self.Destroy()
276
277         def OnQuit(self, e):
278                 self.Close()
279         
280         def OnPreferences(self, e):
281                 prefDialog = preferencesDialog(self)
282                 prefDialog.Centre()
283                 prefDialog.Show(True)
284         
285         def OnCutMesh(self, e):
286                 dlg=wx.FileDialog(self, "Open file to cut", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
287                 dlg.SetWildcard(meshLoader.wildcardFilter())
288                 if dlg.ShowModal() == wx.ID_OK:
289                         filename = dlg.GetPath()
290                         model = meshLoader.loadMesh(filename)
291                         pd = wx.ProgressDialog('Splitting model.', 'Splitting model into multiple parts.', model.vertexCount, self, wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME | wx.PD_SMOOTH)
292                         parts = model.splitToParts(pd.Update)
293                         for part in parts:
294                                 partFilename = filename[:filename.rfind('.')] + "_part%d.stl" % (parts.index(part))
295                                 stl.saveAsSTL(part, partFilename)
296                                 item = ProjectObject(self, partFilename)
297                                 self.list.append(item)
298                                 self.selection = item
299                                 self._updateListbox()
300                                 self.OnListSelect(None)
301                         pd.Destroy()
302                 self.preview.Refresh()
303                 dlg.Destroy()
304         
305         def OnDropFiles(self, filenames):
306                 for filename in filenames:
307                         item = ProjectObject(self, filename)
308                         profile.putPreference('lastFile', item.filename)
309                         self.list.append(item)
310                         self.selection = item
311                         self._updateListbox()
312                 self.OnListSelect(None)
313                 self.preview.Refresh()
314
315         def OnPrintTypeChange(self):
316                 self.printMode = 0
317                 if self.printAllAtOnce.GetValue():
318                         self.printMode = 1
319                 if self.alwaysAutoPlace:
320                         self.OnAutoPlace(None)
321                 self.preview.Refresh()
322         
323         def OnSaveCombinedSTL(self, e):
324                 dlg=wx.FileDialog(self, "Save as STL", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
325                 dlg.SetWildcard("STL files (*.stl)|*.stl;*.STL")
326                 if dlg.ShowModal() == wx.ID_OK:
327                         self._saveCombinedSTL(dlg.GetPath())
328                 dlg.Destroy()
329         
330         def _saveCombinedSTL(self, filename):
331                 totalCount = 0
332                 for item in self.list:
333                         totalCount += item.mesh.vertexCount
334                 output = mesh.mesh()
335                 output._prepareVertexCount(totalCount)
336                 for item in self.list:
337                         offset = numpy.array([item.centerX, item.centerY, 0])
338                         for v in item.mesh.vertexes:
339                                 v0 = v * item.scale + offset
340                                 output.addVertex(v0[0], v0[1], v0[2])
341                 stl.saveAsSTL(output, filename)
342         
343         def OnSaveProject(self, e):
344                 dlg=wx.FileDialog(self, "Save project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
345                 dlg.SetWildcard("Project files (*.curaproject)|*.curaproject")
346                 if dlg.ShowModal() == wx.ID_OK:
347                         cp = ConfigParser.ConfigParser()
348                         i = 0
349                         for item in self.list:
350                                 section = 'model_%d' % (i)
351                                 cp.add_section(section)
352                                 cp.set(section, 'filename', item.filename.encode("utf-8"))
353                                 cp.set(section, 'centerX', str(item.centerX))
354                                 cp.set(section, 'centerY', str(item.centerY))
355                                 cp.set(section, 'scale', str(item.scale))
356                                 cp.set(section, 'rotate', str(item.rotate))
357                                 cp.set(section, 'flipX', str(item.flipX))
358                                 cp.set(section, 'flipY', str(item.flipY))
359                                 cp.set(section, 'flipZ', str(item.flipZ))
360                                 cp.set(section, 'swapXZ', str(item.swapXZ))
361                                 cp.set(section, 'swapYZ', str(item.swapYZ))
362                                 cp.set(section, 'extruder', str(item.extruder+1))
363                                 if item.profile != None:
364                                         cp.set(section, 'profile', item.profile)
365                                 i += 1
366                         cp.write(open(dlg.GetPath(), "w"))
367                 dlg.Destroy()
368
369         def OnLoadProject(self, e):
370                 dlg=wx.FileDialog(self, "Open project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
371                 dlg.SetWildcard("Project files (*.curaproject)|*.curaproject")
372                 if dlg.ShowModal() == wx.ID_OK:
373                         cp = ConfigParser.ConfigParser()
374                         cp.read(dlg.GetPath())
375                         self.list = []
376                         i = 0
377                         while cp.has_section('model_%d' % (i)):
378                                 section = 'model_%d' % (i)
379                                 
380                                 item = ProjectObject(self, unicode(cp.get(section, 'filename'), "utf-8"))
381                                 item.centerX = float(cp.get(section, 'centerX'))
382                                 item.centerY = float(cp.get(section, 'centerY'))
383                                 item.scale = float(cp.get(section, 'scale'))
384                                 item.rotate = float(cp.get(section, 'rotate'))
385                                 item.flipX = cp.get(section, 'flipX') == 'True'
386                                 item.flipY = cp.get(section, 'flipY') == 'True'
387                                 item.flipZ = cp.get(section, 'flipZ') == 'True'
388                                 item.swapXZ = cp.get(section, 'swapXZ') == 'True'
389                                 item.swapYZ = cp.get(section, 'swapYZ') == 'True'
390                                 if cp.has_option(section, 'extruder'):
391                                         item.extuder = int(cp.get(section, 'extruder')) - 1
392                                 if cp.has_option(section, 'profile'):
393                                         item.profile = cp.get(section, 'profile')
394                                 item.updateModelTransform()
395                                 i += 1
396                                 
397                                 self.list.append(item)
398
399                         self.selected = self.list[0]
400                         self._updateListbox()                   
401                         self.OnListSelect(None)
402                         self.preview.Refresh()
403
404                 dlg.Destroy()
405
406         def On3DClick(self):
407                 self.preview.yaw = 30
408                 self.preview.pitch = 60
409                 self.preview.zoom = 300
410                 self.preview.view3D = True
411                 self.preview.Refresh()
412
413         def OnTopClick(self):
414                 self.preview.view3D = False
415                 self.preview.zoom = self.machineSize[0] / 2 + 10
416                 self.preview.offsetX = 0
417                 self.preview.offsetY = 0
418                 self.preview.Refresh()
419
420         def OnListSelect(self, e):
421                 if self.listbox.GetSelection() == -1:
422                         return
423                 self.selection = self.list[self.listbox.GetSelection()]
424                 self.scaleCtrl.SetValue(str(self.selection.scale))
425                 self.rotateCtrl.SetValue(int(self.selection.rotate))
426                 if int(profile.getPreference('extruder_amount')) > 1:
427                         self.extruderCtrl.SetValue(str(self.selection.extruder+1))
428                 self.mirrorX.SetValue(self.selection.flipX)
429                 self.mirrorY.SetValue(self.selection.flipY)
430                 self.mirrorZ.SetValue(self.selection.flipZ)
431                 self.swapXZ.SetValue(self.selection.swapXZ)
432                 self.swapYZ.SetValue(self.selection.swapYZ)
433                 
434                 self.preview.Refresh()
435         
436         def OnAddModel(self, e):
437                 dlg=wx.FileDialog(self, "Open file to print", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE)
438                 dlg.SetWildcard(meshLoader.wildcardFilter())
439                 if dlg.ShowModal() == wx.ID_OK:
440                         for filename in dlg.GetPaths():
441                                 item = ProjectObject(self, filename)
442                                 profile.putPreference('lastFile', item.filename)
443                                 self.list.append(item)
444                                 self.selection = item
445                                 self._updateListbox()
446                                 self.OnListSelect(None)
447                 self.preview.Refresh()
448                 dlg.Destroy()
449         
450         def OnRemModel(self, e):
451                 if self.selection is None:
452                         return
453                 self.list.remove(self.selection)
454                 self._updateListbox()
455                 self.preview.Refresh()
456         
457         def OnMoveUp(self, e):
458                 if self.selection is None:
459                         return
460                 i = self.listbox.GetSelection()
461                 if i == 0:
462                         return
463                 self.list.remove(self.selection)
464                 self.list.insert(i-1, self.selection)
465                 self._updateListbox()
466                 self.preview.Refresh()
467
468         def OnMoveDown(self, e):
469                 if self.selection is None:
470                         return
471                 i = self.listbox.GetSelection()
472                 if i == len(self.list) - 1:
473                         return
474                 self.list.remove(self.selection)
475                 self.list.insert(i+1, self.selection)
476                 self._updateListbox()
477                 self.preview.Refresh()
478         
479         def OnCopy(self, e):
480                 if self.selection is None:
481                         return
482                 
483                 item = self.selection.clone()
484                 self.list.insert(self.list.index(self.selection), item)
485                 self.selection = item
486                 
487                 self._updateListbox()
488                 self.preview.Refresh()
489         
490         def OnSetCustomProfile(self, e):
491                 if self.selection is None:
492                         return
493
494                 dlg=wx.FileDialog(self, "Select profile", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
495                 dlg.SetWildcard("Profile files (*.ini)|*.ini;*.INI")
496                 if dlg.ShowModal() == wx.ID_OK:
497                         self.selection.profile = dlg.GetPath()
498                 else:
499                         self.selection.profile = None
500                 self._updateListbox()
501                 dlg.Destroy()
502         
503         def _updateListbox(self):
504                 self.listbox.Clear()
505                 for item in self.list:
506                         if item.profile is not None:
507                                 self.listbox.AppendAndEnsureVisible(os.path.split(item.filename)[1] + " *")
508                         else:
509                                 self.listbox.AppendAndEnsureVisible(os.path.split(item.filename)[1])
510                 if self.selection in self.list:
511                         self.listbox.SetSelection(self.list.index(self.selection))
512                 elif len(self.list) > 0:
513                         self.selection = self.list[0]
514                         self.listbox.SetSelection(0)
515                 else:
516                         self.selection = None
517                         self.listbox.SetSelection(-1)
518                 if self.alwaysAutoPlace:
519                         self.OnAutoPlace(None)
520
521         def OnAutoPlace(self, e):
522                 bestAllowedSize = int(self.machineSize[1])
523                 bestArea = self._doAutoPlace(bestAllowedSize)
524                 for i in xrange(10, int(self.machineSize[1]), 10):
525                         area = self._doAutoPlace(i)
526                         if area < bestArea:
527                                 bestAllowedSize = i
528                                 bestArea = area
529                 self._doAutoPlace(bestAllowedSize)
530                 if not self.alwaysAutoPlace:
531                         for item in self.list:
532                                 item.clampXY()
533                 self.preview.Refresh()
534         
535         def _doAutoPlace(self, allowedSizeY):
536                 extraSizeMin, extraSizeMax = self.getExtraHeadSize()
537
538                 if extraSizeMin[0] > extraSizeMax[0]:
539                         posX = self.machineSize[0]
540                         dirX = -1
541                 else:
542                         posX = 0
543                         dirX = 1
544                 posY = 0
545                 dirY = 1
546                 
547                 minX = self.machineSize[0]
548                 minY = self.machineSize[1]
549                 maxX = 0
550                 maxY = 0
551                 for item in self.list:
552                         item.centerX = posX + item.getMaximum()[0] * item.scale * dirX
553                         item.centerY = posY + item.getMaximum()[1] * item.scale * dirY
554                         if item.centerY + item.getSize()[1] >= allowedSizeY:
555                                 if dirX < 0:
556                                         posX = minX - extraSizeMax[0] - 1
557                                 else:
558                                         posX = maxX + extraSizeMin[0] + 1
559                                 posY = 0
560                                 item.centerX = posX + item.getMaximum()[0] * item.scale * dirX
561                                 item.centerY = posY + item.getMaximum()[1] * item.scale * dirY
562                         posY += item.getSize()[1]  * item.scale * dirY + extraSizeMin[1] + 1
563                         minX = min(minX, item.centerX - item.getSize()[0] * item.scale / 2)
564                         minY = min(minY, item.centerY - item.getSize()[1] * item.scale / 2)
565                         maxX = max(maxX, item.centerX + item.getSize()[0] * item.scale / 2)
566                         maxY = max(maxY, item.centerY + item.getSize()[1] * item.scale / 2)
567                 
568                 for item in self.list:
569                         if dirX < 0:
570                                 item.centerX -= minX / 2
571                         else:
572                                 item.centerX += (self.machineSize[0] - maxX) / 2
573                         item.centerY += (self.machineSize[1] - maxY) / 2
574                 
575                 if minX < 0 or maxX > self.machineSize[0]:
576                         return ((maxX - minX) + (maxY - minY)) * 100
577                 
578                 return (maxX - minX) + (maxY - minY)
579
580         def OnSlice(self, e):
581                 dlg=wx.FileDialog(self, "Save project gcode file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
582                 dlg.SetWildcard("GCode file (*.gcode)|*.gcode")
583                 if dlg.ShowModal() != wx.ID_OK:
584                         dlg.Destroy()
585                         return
586                 resultFilename = dlg.GetPath()
587                 dlg.Destroy()
588
589                 put = profile.setTempOverride
590                 oldProfile = profile.getGlobalProfileString()
591                 
592                 if self.printMode == 0:
593                         fileList = []
594                         positionList = []
595                         for item in self.list:
596                                 fileList.append(item.filename)
597                                 if profile.getPreference('machine_center_is_zero') == 'True':
598                                         pos = [item.centerX - self.machineSize[0] / 2, item.centerY - self.machineSize[1] / 2]
599                                 else:
600                                         pos = [item.centerX, item.centerY]
601                                 positionList.append(pos + (item.mesh.matrix * item.scale).reshape((9,)).tolist())
602                         sliceCommand = sliceRun.getSliceCommand(resultFilename, fileList, positionList)
603                 else:
604                         self._saveCombinedSTL(resultFilename + "_temp_.stl")
605                         sliceCommand = sliceRun.getSliceCommand(resultFilename, [resultFilename + "_temp_.stl"], [profile.getMachineCenterCoords()])
606
607                 pspw = ProjectSliceProgressWindow(sliceCommand, resultFilename, len(self.list))
608                 pspw.Centre()
609                 pspw.Show(True)
610
611         def OnScaleChange(self, e):
612                 if self.selection is None:
613                         return
614                 try:
615                         self.selection.scale = float(self.scaleCtrl.GetValue())
616                 except ValueError:
617                         self.selection.scale = 1.0
618                 if self.alwaysAutoPlace:
619                         self.OnAutoPlace(None)
620                 self.preview.Refresh()
621         
622         def OnRotateChange(self, e):
623                 if self.selection is None:
624                         return
625                 self.selection.rotate = float(self.rotateCtrl.GetValue())
626                 self.selection.updateModelTransform()
627                 if self.alwaysAutoPlace:
628                         self.OnAutoPlace(None)
629                 self.preview.Refresh()
630
631         def OnExtruderChange(self, e):
632                 if self.selection is None:
633                         return
634                 self.selection.extruder = int(self.extruderCtrl.GetValue()) - 1
635                 self.preview.Refresh()
636                 
637         def OnMirrorChange(self):
638                 if self.selection is None:
639                         return
640                 self.selection.flipX = self.mirrorX.GetValue()
641                 self.selection.flipY = self.mirrorY.GetValue()
642                 self.selection.flipZ = self.mirrorZ.GetValue()
643                 self.selection.swapXZ = self.swapXZ.GetValue()
644                 self.selection.swapYZ = self.swapYZ.GetValue()
645                 self.selection.updateModelTransform()
646                 if self.alwaysAutoPlace:
647                         self.OnAutoPlace(None)
648                 self.preview.Refresh()
649
650         def getExtraHeadSize(self):
651                 extraSizeMin = self.headSizeMin
652                 extraSizeMax = self.headSizeMax
653                 if profile.getProfileSettingFloat('skirt_line_count') > 0:
654                         skirtSize = profile.getProfileSettingFloat('skirt_line_count') * profile.calculateEdgeWidth() + profile.getProfileSettingFloat('skirt_gap')
655                         extraSizeMin = extraSizeMin + numpy.array([skirtSize, skirtSize, 0])
656                         extraSizeMax = extraSizeMax + numpy.array([skirtSize, skirtSize, 0])
657                 if profile.getProfileSetting('enable_raft') != 'False':
658                         raftSize = profile.getProfileSettingFloat('raft_margin') * 2
659                         extraSizeMin = extraSizeMin + numpy.array([raftSize, raftSize, 0])
660                         extraSizeMax = extraSizeMax + numpy.array([raftSize, raftSize, 0])
661                 if profile.getProfileSetting('support') != 'None':
662                         extraSizeMin = extraSizeMin + numpy.array([3.0, 0, 0])
663                         extraSizeMax = extraSizeMax + numpy.array([3.0, 0, 0])
664
665                 if self.printMode == 1:
666                         extraSizeMin = numpy.array([6.0, 6.0, 0])
667                         extraSizeMax = numpy.array([6.0, 6.0, 0])
668                 
669                 return extraSizeMin, extraSizeMax
670
671 class PreviewGLCanvas(glcanvas.GLCanvas):
672         def __init__(self, parent, projectPlannerWindow):
673                 attribList = (glcanvas.WX_GL_RGBA, glcanvas.WX_GL_DOUBLEBUFFER, glcanvas.WX_GL_DEPTH_SIZE, 24, glcanvas.WX_GL_STENCIL_SIZE, 8)
674                 glcanvas.GLCanvas.__init__(self, parent, attribList = attribList)
675                 self.parent = projectPlannerWindow
676                 self.context = glcanvas.GLContext(self)
677                 wx.EVT_PAINT(self, self.OnPaint)
678                 wx.EVT_SIZE(self, self.OnSize)
679                 wx.EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)
680                 wx.EVT_LEFT_DOWN(self, self.OnMouseLeftDown)
681                 wx.EVT_MOTION(self, self.OnMouseMotion)
682                 wx.EVT_MOUSEWHEEL(self, self.OnMouseWheel)
683                 self.yaw = 30
684                 self.pitch = 60
685                 self.offsetX = 0
686                 self.offsetY = 0
687                 self.view3D = self.parent.alwaysAutoPlace
688                 if self.view3D:
689                         self.zoom = 300
690                 else:
691                         self.zoom = self.parent.machineSize[0] / 2 + 10
692                 self.allowDrag = False
693
694                 self.objColor = profile.getPreferenceColour('model_colour')
695
696         def OnMouseLeftDown(self,e):
697                 self.allowDrag = True
698                 if not self.parent.alwaysAutoPlace and not self.view3D:
699                         p0 = numpy.array(gluUnProject(e.GetX(), self.viewport[1] + self.viewport[3] - e.GetY(), 0, self.modelMatrix, self.projMatrix, self.viewport))
700                         p1 = numpy.array(gluUnProject(e.GetX(), self.viewport[1] + self.viewport[3] - e.GetY(), 1, self.modelMatrix, self.projMatrix, self.viewport))
701                         cursorZ0 = p0 - (p1 - p0) * (p0[2] / (p1[2] - p0[2]))
702
703                         for item in self.parent.list:
704                                 iMin = (item.getMinimum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) - self.parent.extruderOffset[item.extruder]
705                                 iMax = (item.getMaximum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) - self.parent.extruderOffset[item.extruder]
706                                 if iMin[0] <= cursorZ0[0] <= iMax[0] and iMin[1] <= cursorZ0[1] <= iMax[1]:
707                                         self.parent.selection = item
708                                         self.parent._updateListbox()
709                                         self.parent.OnListSelect(None)
710
711         def OnMouseMotion(self,e):
712                 if self.allowDrag and e.Dragging() and e.LeftIsDown():
713                         if self.view3D:
714                                 self.yaw += e.GetX() - self.oldX
715                                 self.pitch -= e.GetY() - self.oldY
716                                 if self.pitch > 170:
717                                         self.pitch = 170
718                                 if self.pitch < 10:
719                                         self.pitch = 10
720                         elif not self.parent.alwaysAutoPlace:
721                                 item = self.parent.selection
722                                 if item is not None:
723                                         item.centerX += float(e.GetX() - self.oldX) * self.zoom / self.GetSize().GetHeight() * 2
724                                         item.centerY -= float(e.GetY() - self.oldY) * self.zoom / self.GetSize().GetHeight() * 2
725                                         item.clampXY()
726                         self.Refresh()
727                 else:
728                         self.allowDrag = False
729                 if e.Dragging() and e.RightIsDown():
730                         if self.view3D:
731                                 self.zoom += e.GetY() - self.oldY
732                                 if self.zoom < 1:
733                                         self.zoom = 1
734                         self.Refresh()
735                 self.oldX = e.GetX()
736                 self.oldY = e.GetY()
737         
738         def OnMouseWheel(self,e):
739                 if self.view3D:
740                         self.zoom *= 1.0 - float(e.GetWheelRotation() / e.GetWheelDelta()) / 10.0
741                         if self.zoom < 1.0:
742                                 self.zoom = 1.0
743                         self.Refresh()
744         
745         def OnEraseBackground(self,event):
746                 #Workaround for windows background redraw flicker.
747                 pass
748         
749         def OnSize(self,event):
750                 self.Refresh()
751
752         def OnPaint(self,event):
753                 dc = wx.PaintDC(self)
754                 if not hasOpenGLlibs:
755                         dc.Clear()
756                         dc.DrawText("No PyOpenGL installation found.\nNo preview window available.", 10, 10)
757                         return
758                 self.SetCurrent(self.context)
759                 opengl.InitGL(self, self.view3D, self.zoom)
760                 if self.view3D:
761                         glTranslate(0,0,-self.zoom)
762                         glRotate(-self.pitch, 1,0,0)
763                         glRotate(self.yaw, 0,0,1)
764                 else:
765                         glTranslate(self.offsetX, self.offsetY, 0.0)
766                 glTranslate(-self.parent.machineSize[0]/2, -self.parent.machineSize[1]/2, 0)
767
768                 self.viewport = glGetIntegerv(GL_VIEWPORT);
769                 self.modelMatrix = glGetDoublev(GL_MODELVIEW_MATRIX);
770                 self.projMatrix = glGetDoublev(GL_PROJECTION_MATRIX);
771
772                 self.OnDraw()
773                 self.SwapBuffers()
774
775         def OnDraw(self):
776                 machineSize = self.parent.machineSize
777                 extraSizeMin, extraSizeMax = self.parent.getExtraHeadSize()
778
779                 for item in self.parent.list:
780                         item.validPlacement = True
781                         item.gotHit = False
782                 
783                 for idx1 in xrange(0, len(self.parent.list)):
784                         item = self.parent.list[idx1]
785                         iMin1 = (item.getMinimum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) - extraSizeMin - self.parent.extruderOffset[item.extruder]
786                         iMax1 = (item.getMaximum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) + extraSizeMax - self.parent.extruderOffset[item.extruder]
787                         if iMin1[0] < -self.parent.headSizeMin[0] or iMin1[1] < -self.parent.headSizeMin[1]:
788                                 item.validPlacement = False
789                         if iMax1[0] > machineSize[0] + self.parent.headSizeMax[0] or iMax1[1] > machineSize[1] + self.parent.headSizeMax[1]:
790                                 item.validPlacement = False
791                         for idx2 in xrange(0, idx1):
792                                 item2 = self.parent.list[idx2]
793                                 iMin2 = (item2.getMinimum() * item2.scale) + numpy.array([item2.centerX, item2.centerY, 0])
794                                 iMax2 = (item2.getMaximum() * item2.scale) + numpy.array([item2.centerX, item2.centerY, 0])
795                                 if item != item2 and iMax1[0] >= iMin2[0] and iMin1[0] <= iMax2[0] and iMax1[1] >= iMin2[1] and iMin1[1] <= iMax2[1]:
796                                         item.validPlacement = False
797                                         item2.gotHit = True
798                 
799                 seenSelected = False
800                 for item in self.parent.list:
801                         if item == self.parent.selection:
802                                 seenSelected = True
803                         if item.modelDisplayList is None:
804                                 item.modelDisplayList = glGenLists(1);
805                         if item.modelDirty:
806                                 item.modelDirty = False
807                                 modelSize = item.getMaximum() - item.getMinimum()
808                                 glNewList(item.modelDisplayList, GL_COMPILE)
809                                 opengl.DrawMesh(item.mesh)
810                                 glEndList()
811                         
812                         if item.validPlacement:
813                                 if self.parent.selection == item:
814                                         glLightfv(GL_LIGHT0, GL_DIFFUSE,  map(lambda x: x + 0.2, self.objColor))
815                                         glLightfv(GL_LIGHT0, GL_AMBIENT,  map(lambda x: x / 2, self.objColor))
816                                 else:
817                                         glLightfv(GL_LIGHT0, GL_DIFFUSE,  self.objColor)
818                                         glLightfv(GL_LIGHT0, GL_AMBIENT,  map(lambda x: x / 2, self.objColor))
819                         else:
820                                 if self.parent.selection == item:
821                                         glLightfv(GL_LIGHT0, GL_DIFFUSE,  [1.0, 0.0, 0.0, 0.0])
822                                         glLightfv(GL_LIGHT0, GL_AMBIENT,  [0.2, 0.0, 0.0, 0.0])
823                                 else:
824                                         glLightfv(GL_LIGHT0, GL_DIFFUSE,  [1.0, 0.0, 0.0, 0.0])
825                                         glLightfv(GL_LIGHT0, GL_AMBIENT,  [0.2, 0.0, 0.0, 0.0])
826                         glPushMatrix()
827                         
828                         glEnable(GL_LIGHTING)
829                         glTranslate(item.centerX, item.centerY, 0)
830                         glPushMatrix()
831                         glScalef(item.scale, item.scale, item.scale)
832                         glCallList(item.modelDisplayList)
833                         glPopMatrix()
834                         
835                         vMin = item.getMinimum() * item.scale
836                         vMax = item.getMaximum() * item.scale
837                         vMinHead = vMin - extraSizeMin - self.parent.extruderOffset[item.extruder]
838                         vMaxHead = vMax + extraSizeMax - self.parent.extruderOffset[item.extruder]
839
840                         glDisable(GL_LIGHTING)
841
842                         if not self.parent.alwaysAutoPlace:
843                                 if self.parent.selection == item:
844                                         if item.gotHit:
845                                                 glColor3f(1.0,0.0,0.3)
846                                         else:
847                                                 glColor3f(1.0,0.0,1.0)
848                                         opengl.DrawBox(vMin, vMax)
849                                         if item.gotHit:
850                                                 glColor3f(1.0,0.3,0.0)
851                                         else:
852                                                 glColor3f(1.0,1.0,0.0)
853                                         opengl.DrawBox(vMinHead, vMaxHead)
854                                 elif seenSelected:
855                                         if item.gotHit:
856                                                 glColor3f(0.5,0.0,0.1)
857                                         else:
858                                                 glColor3f(0.5,0.0,0.5)
859                                         opengl.DrawBox(vMinHead, vMaxHead)
860                                 else:
861                                         if item.gotHit:
862                                                 glColor3f(0.7,0.1,0.0)
863                                         else:
864                                                 glColor3f(0.7,0.7,0.0)
865                                         opengl.DrawBox(vMin, vMax)
866                         
867                         glPopMatrix()
868                 
869                 opengl.DrawMachine(util3d.Vector3(machineSize[0], machineSize[1], machineSize[2]))
870                 glFlush()
871
872 class ProjectSliceProgressWindow(wx.Frame):
873         def __init__(self, sliceCommand, resultFilename, fileCount):
874                 super(ProjectSliceProgressWindow, self).__init__(None, title='Cura')
875                 self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
876                 
877                 self.sliceCommand = sliceCommand
878                 self.resultFilename = resultFilename
879                 self.fileCount = fileCount
880                 self.abort = False
881                 self.prevStep = 'start'
882                 self.totalDoneFactor = 0.0
883                 self.startTime = time.time()
884                 self.sliceStartTime = time.time()
885                 
886                 self.sizer = wx.GridBagSizer(2, 2) 
887                 self.statusText = wx.StaticText(self, -1, "Building: %s" % (resultFilename))
888                 self.progressGauge = wx.Gauge(self, -1)
889                 self.progressGauge.SetRange(10000)
890                 self.progressGauge2 = wx.Gauge(self, -1)
891                 self.progressGauge2.SetRange(self.fileCount)
892                 self.progressGauge2.SetValue(-1)
893                 self.abortButton = wx.Button(self, -1, "Abort")
894                 self.sizer.Add(self.statusText, (0,0), span=(1,5))
895                 self.sizer.Add(self.progressGauge, (1, 0), span=(1,5), flag=wx.EXPAND)
896                 self.sizer.Add(self.progressGauge2, (2, 0), span=(1,5), flag=wx.EXPAND)
897
898                 self.sizer.Add(self.abortButton, (3,0), span=(1,5), flag=wx.ALIGN_CENTER)
899                 self.sizer.AddGrowableCol(0)
900                 self.sizer.AddGrowableRow(0)
901
902                 self.Bind(wx.EVT_BUTTON, self.OnAbort, self.abortButton)
903                 self.SetSizer(self.sizer)
904                 self.Layout()
905                 self.Fit()
906                 
907                 threading.Thread(target=self.OnRun).start()
908
909         def OnAbort(self, e):
910                 if self.abort:
911                         self.Close()
912                 else:
913                         self.abort = True
914                         self.abortButton.SetLabel('Close')
915
916         def SetProgress(self, stepName, layer, maxLayer):
917                 if self.prevStep != stepName:
918                         if stepName == 'slice':
919                                 self.progressGauge2.SetValue(self.progressGauge2.GetValue() + 1)
920                                 self.totalDoneFactor = 0
921                         self.totalDoneFactor += sliceRun.sliceStepTimeFactor[self.prevStep]
922                         newTime = time.time()
923                         #print "#####" + str(newTime-self.startTime) + " " + self.prevStep + " -> " + stepName
924                         self.startTime = newTime
925                         self.prevStep = stepName
926                 
927                 progresValue = ((self.totalDoneFactor + sliceRun.sliceStepTimeFactor[stepName] * layer / maxLayer) / sliceRun.totalRunTimeFactor) * 10000
928                 self.progressGauge.SetValue(int(progresValue))
929                 self.statusText.SetLabel(stepName + " [" + str(layer) + "/" + str(maxLayer) + "]")
930                 taskbar.setProgress(self, 10000 * self.progressGauge2.GetValue() + int(progresValue), 10000 * self.fileCount)
931         
932         def OnRun(self):
933                 self.progressLog = []
934                 p = sliceRun.startSliceCommandProcess(self.sliceCommand)
935                 line = p.stdout.readline()
936                 while(len(line) > 0):
937                         line = line.rstrip()
938                         if line[0:9] == "Progress[" and line[-1:] == "]":
939                                 progress = line[9:-1].split(":")
940                                 if len(progress) > 2:
941                                         maxValue = int(progress[2])
942                                 wx.CallAfter(self.SetProgress, progress[0], int(progress[1]), maxValue)
943                         else:
944                                 self.progressLog.append(line)
945                                 wx.CallAfter(self.statusText.SetLabel, line)
946                         if self.abort:
947                                 p.terminate()
948                                 wx.CallAfter(self.statusText.SetLabel, "Aborted by user.")
949                                 return
950                         line = p.stdout.readline()
951                 line = p.stderr.readline()
952                 while len(line) > 0:
953                         line = line.rstrip()
954                         self.progressLog.append(line)
955                         line = p.stderr.readline()
956                 self.returnCode = p.wait()
957                 self.progressGauge2.SetValue(self.fileCount)
958
959                 gcode = gcodeInterpreter.gcode()
960                 gcode.load(self.resultFilename)
961                 
962                 self.abort = True
963                 sliceTime = time.time() - self.sliceStartTime
964                 status = "Build: %s" % (self.resultFilename)
965                 status += "\nSlicing took: %02d:%02d" % (sliceTime / 60, sliceTime % 60)
966                 status += "\nFilament: %.2fm %.2fg" % (gcode.extrusionAmount / 1000, gcode.calculateWeight() * 1000)
967                 status += "\nPrint time: %02d:%02d" % (int(gcode.totalMoveTimeMinute / 60), int(gcode.totalMoveTimeMinute % 60))
968                 cost = gcode.calculateCost()
969                 if cost is not None:
970                         status += "\nCost: %s" % (cost)
971                 profile.replaceGCodeTags(self.resultFilename, gcode)
972                 wx.CallAfter(self.statusText.SetLabel, status)
973                 wx.CallAfter(self.OnSliceDone)
974         
975         def _adjustNumberInLine(self, line, tag, f):
976                 m = re.search('^(.*'+tag+')([0-9\.]*)(.*)$', line)
977                 return m.group(1) + str(float(m.group(2)) + f) + m.group(3) + '\n'
978         
979         def OnSliceDone(self):
980                 self.abortButton.Destroy()
981                 self.closeButton = wx.Button(self, -1, "Close")
982                 self.printButton = wx.Button(self, -1, "Print")
983                 self.logButton = wx.Button(self, -1, "Show log")
984                 self.sizer.Add(self.closeButton, (3,0), span=(1,1))
985                 self.sizer.Add(self.printButton, (3,1), span=(1,1))
986                 self.sizer.Add(self.logButton, (3,2), span=(1,1))
987                 if exporer.hasExporer():
988                         self.openFileLocationButton = wx.Button(self, -1, "Open file location")
989                         self.Bind(wx.EVT_BUTTON, self.OnOpenFileLocation, self.openFileLocationButton)
990                         self.sizer.Add(self.openFileLocationButton, (3,3), span=(1,1))
991                 if profile.getPreference('sdpath') != '':
992                         self.copyToSDButton = wx.Button(self, -1, "To SDCard")
993                         self.Bind(wx.EVT_BUTTON, self.OnCopyToSD, self.copyToSDButton)
994                         self.sizer.Add(self.copyToSDButton, (3,4), span=(1,1))
995                 self.Bind(wx.EVT_BUTTON, self.OnAbort, self.closeButton)
996                 self.Bind(wx.EVT_BUTTON, self.OnPrint, self.printButton)
997                 self.Bind(wx.EVT_BUTTON, self.OnShowLog, self.logButton)
998                 self.Layout()
999                 self.Fit()
1000                 taskbar.setBusy(self, False)
1001
1002         def OnCopyToSD(self, e):
1003                 filename = os.path.basename(self.resultFilename)
1004                 if profile.getPreference('sdshortnames') == 'True':
1005                         filename = sliceRun.getShortFilename(filename)
1006                 shutil.copy(self.resultFilename, os.path.join(profile.getPreference('sdpath'), filename))
1007         
1008         def OnOpenFileLocation(self, e):
1009                 exporer.openExporer(self.resultFilename)
1010         
1011         def OnPrint(self, e):
1012                 printWindow.printFile(self.resultFilename)
1013
1014         def OnShowLog(self, e):
1015                 LogWindow('\n'.join(self.progressLog))
1016
1017 class preferencesDialog(wx.Frame):
1018         def __init__(self, parent):
1019                 super(preferencesDialog, self).__init__(None, title="Project Planner Preferences", style=wx.DEFAULT_DIALOG_STYLE)
1020                 
1021                 self.parent = parent
1022                 wx.EVT_CLOSE(self, self.OnClose)
1023
1024                 self.panel = configBase.configPanelBase(self)
1025                 extruderAmount = int(profile.getPreference('extruder_amount'))
1026                 
1027                 left, right, main = self.panel.CreateConfigPanel(self)
1028                 configBase.TitleRow(left, 'User interface settings')
1029                 c = configBase.SettingRow(left, 'Always auto place objects in planner', 'planner_always_autoplace', True, 'Disable this to allow manual placement in the project planner (requires restart).', type = 'preference')
1030                 configBase.TitleRow(left, 'Machine head size')
1031                 c = configBase.SettingRow(left, 'Head size - X towards home (mm)', 'extruder_head_size_min_x', '0', 'Size of your printer head in the X direction, on the Ultimaker your fan is in this direction.', type = 'preference')
1032                 validators.validFloat(c, 0.1)
1033                 c = configBase.SettingRow(left, 'Head size - X towards end (mm)', 'extruder_head_size_max_x', '0', 'Size of your printer head in the X direction.', type = 'preference')
1034                 validators.validFloat(c, 0.1)
1035                 c = configBase.SettingRow(left, 'Head size - Y towards home (mm)', 'extruder_head_size_min_y', '0', 'Size of your printer head in the Y direction.', type = 'preference')
1036                 validators.validFloat(c, 0.1)
1037                 c = configBase.SettingRow(left, 'Head size - Y towards end (mm)', 'extruder_head_size_max_y', '0', 'Size of your printer head in the Y direction.', type = 'preference')
1038                 validators.validFloat(c, 0.1)
1039                 c = configBase.SettingRow(left, 'Head gantry height (mm)', 'extruder_head_size_height', '0', 'The tallest object height that will always fit under your printers gantry system when the printer head is at the lowest Z position.', type = 'preference')
1040                 validators.validFloat(c)
1041                 
1042                 self.okButton = wx.Button(left, -1, 'Ok')
1043                 left.GetSizer().Add(self.okButton, (left.GetSizer().GetRows(), 1))
1044                 self.okButton.Bind(wx.EVT_BUTTON, self.OnClose)
1045                 
1046                 self.MakeModal(True)
1047                 main.Fit()
1048                 self.Fit()
1049
1050         def OnClose(self, e):
1051                 self.parent.headSizeMin = numpy.array([profile.getPreferenceFloat('extruder_head_size_min_x'), profile.getPreferenceFloat('extruder_head_size_min_y'),0])
1052                 self.parent.headSizeMax = numpy.array([profile.getPreferenceFloat('extruder_head_size_max_x'), profile.getPreferenceFloat('extruder_head_size_max_y'),0])
1053                 self.parent.Refresh()
1054
1055                 self.MakeModal(False)
1056                 self.Destroy()
1057
1058 class LogWindow(wx.Frame):
1059         def __init__(self, logText):
1060                 super(LogWindow, self).__init__(None, title="Slice log")
1061                 self.textBox = wx.TextCtrl(self, -1, logText, style=wx.TE_MULTILINE|wx.TE_DONTWRAP|wx.TE_READONLY)
1062                 self.SetSize((400,300))
1063                 self.Centre()
1064                 self.Show(True)