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