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