1 from __future__ import absolute_import
12 from wx import glcanvas
15 OpenGL.ERROR_CHECKING = False
16 from OpenGL.GLU import *
17 from OpenGL.GL import *
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
38 class ProjectObject(object):
39 def __init__(self, parent, filename):
40 super(ProjectObject, self).__init__()
42 self.mesh = meshLoader.loadMesh(filename)
45 self.filename = filename
46 self.matrix = numpy.matrix([[1,0,0],[0,1,0],[0,0,1]], numpy.float64)
48 self.modelDisplayList = None
49 self.modelDirty = True
51 self.centerX = -self.getSize()[0]/2 + 5
52 self.centerY = -self.getSize()[1]/2 + 5
54 self.updateModelTransform()
56 def isSameExceptForPosition(self, other):
57 if self.filename != other.filename:
59 if self.scale != other.scale:
61 if self.rotate != other.rotate:
63 if self.flipX != other.flipX:
65 if self.flipY != other.flipY:
67 if self.flipZ != other.flipZ:
69 if self.swapXZ != other.swapXZ:
71 if self.swapYZ != other.swapYZ:
73 if self.extruder != other.extruder:
75 if self.profile != other.profile:
79 def updateModelTransform(self):
80 self.mesh.processMatrix()
83 return self.mesh.getMinimum()
85 return self.mesh.getMaximum()
87 return self.mesh.getSize()
90 p = ProjectObject(self.parent, self.filename)
92 p.centerX = self.centerX + 5
93 p.centerY = self.centerY + 5
95 p.filename = self.filename
97 p.rotate = self.rotate
101 p.swapXZ = self.swapXZ
102 p.swapYZ = self.swapYZ
103 p.extruder = self.extruder
104 p.profile = self.profile
106 p.updateModelTransform()
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
120 class projectPlanner(wx.Frame):
121 "Main user interface window"
123 super(projectPlanner, self).__init__(None, title='Cura - Project Planner')
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)
130 self.SetDropTarget(dropTarget.FileDropTarget(self.OnDropFiles, meshLoader.supportedExtensions()))
133 self.selection = None
135 self.alwaysAutoPlace = profile.getPreference('planner_always_autoplace') == 'True'
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])
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])]
147 self.toolbar = toolbarUtil.Toolbar(self.panel)
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()
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()
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')
167 self.toolbar.Realize()
169 self.toolbar2 = toolbarUtil.Toolbar(self.panel)
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()
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()
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()
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")
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)
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)
225 panel = wx.Panel(self.panel, -1)
226 sizer.Add(panel, (5,1), span=(1,2))
228 sizer = wx.GridBagSizer(2,2)
229 panel.SetSizer(sizer)
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)
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)
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)
246 self.scaleCtrl.Bind(wx.EVT_TEXT, self.OnScaleChange)
247 self.rotateCtrl.Bind(wx.EVT_SPINCTRL, self.OnRotateChange)
249 self.SetSize((800,600))
251 def OnClose(self, e):
257 def OnPreferences(self, e):
258 prefDialog = preferencesDialog(self)
260 prefDialog.Show(True)
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)
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)
279 self.preview.Refresh()
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()
292 def OnPrintTypeChange(self):
294 if self.printAllAtOnce.GetValue():
296 if self.alwaysAutoPlace:
297 self.OnAutoPlace(None)
298 self.preview.Refresh()
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())
307 def _saveCombinedSTL(self, filename):
309 for item in self.list:
310 totalCount += item.mesh.vertexCount
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)
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()
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)
343 cp.write(open(dlg.GetPath(), "w"))
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())
354 while cp.has_section('model_%d' % (i)):
355 section = 'model_%d' % (i)
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()
374 self.list.append(item)
376 self.selected = self.list[0]
377 self._updateListbox()
378 self.OnListSelect(None)
379 self.preview.Refresh()
384 self.preview.yaw = 30
385 self.preview.pitch = 60
386 self.preview.zoom = 300
387 self.preview.view3D = True
388 self.preview.Refresh()
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()
397 def OnListSelect(self, e):
398 if self.listbox.GetSelection() == -1:
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)
411 self.preview.Refresh()
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()
427 def OnRemModel(self, e):
428 if self.selection is None:
430 self.list.remove(self.selection)
431 self._updateListbox()
432 self.preview.Refresh()
434 def OnMoveUp(self, e):
435 if self.selection is None:
437 i = self.listbox.GetSelection()
440 self.list.remove(self.selection)
441 self.list.insert(i-1, self.selection)
442 self._updateListbox()
443 self.preview.Refresh()
445 def OnMoveDown(self, e):
446 if self.selection is None:
448 i = self.listbox.GetSelection()
449 if i == len(self.list) - 1:
451 self.list.remove(self.selection)
452 self.list.insert(i+1, self.selection)
453 self._updateListbox()
454 self.preview.Refresh()
457 if self.selection is None:
460 item = self.selection.clone()
461 self.list.insert(self.list.index(self.selection), item)
462 self.selection = item
464 self._updateListbox()
465 self.preview.Refresh()
467 def OnSetCustomProfile(self, e):
468 if self.selection is None:
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()
476 self.selection.profile = None
477 self._updateListbox()
480 def _updateListbox(self):
482 for item in self.list:
483 if item.profile is not None:
484 self.listbox.AppendAndEnsureVisible(os.path.split(item.filename)[1] + " *")
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)
493 self.selection = None
494 self.listbox.SetSelection(-1)
495 if self.alwaysAutoPlace:
496 self.OnAutoPlace(None)
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)
506 self._doAutoPlace(bestAllowedSize)
507 if not self.alwaysAutoPlace:
508 for item in self.list:
510 self.preview.Refresh()
512 def _doAutoPlace(self, allowedSizeY):
513 extraSizeMin, extraSizeMax = self.getExtraHeadSize()
515 if extraSizeMin[0] > extraSizeMax[0]:
516 posX = self.machineSize[0]
524 minX = self.machineSize[0]
525 minY = self.machineSize[1]
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:
533 posX = minX - extraSizeMax[0] - 1
535 posX = maxX + extraSizeMin[0] + 1
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)
545 for item in self.list:
547 item.centerX -= minX / 2
549 item.centerX += (self.machineSize[0] - maxX) / 2
550 item.centerY += (self.machineSize[1] - maxY) / 2
552 if minX < 0 or maxX > self.machineSize[0]:
553 return ((maxX - minX) + (maxY - minY)) * 100
555 return (maxX - minX) + (maxY - minY)
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:
563 resultFilename = dlg.GetPath()
566 put = profile.setTempOverride
567 oldProfile = profile.getGlobalProfileString()
569 if self.printMode == 0:
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]
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)
581 self._saveCombinedSTL(resultFilename + "_temp_.stl")
582 sliceCommand = sliceRun.getSliceCommand(resultFilename, [resultFilename + "_temp_.stl"], [profile.getMachineCenterCoords()])
584 pspw = ProjectSliceProgressWindow(sliceCommand, resultFilename, len(self.list))
588 def OnScaleChange(self, e):
589 if self.selection is None:
592 self.selection.scale = float(self.scaleCtrl.GetValue())
594 self.selection.scale = 1.0
595 if self.alwaysAutoPlace:
596 self.OnAutoPlace(None)
597 self.preview.Refresh()
599 def OnRotateChange(self, e):
600 if self.selection is None:
602 self.selection.rotate = float(self.rotateCtrl.GetValue())
603 self.selection.updateModelTransform()
604 if self.alwaysAutoPlace:
605 self.OnAutoPlace(None)
606 self.preview.Refresh()
608 def OnExtruderChange(self, e):
609 if self.selection is None:
611 self.selection.extruder = int(self.extruderCtrl.GetValue()) - 1
612 self.preview.Refresh()
614 def OnMirrorChange(self):
615 if self.selection is None:
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()
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])
642 if self.printMode == 1:
643 extraSizeMin = numpy.array([6.0, 6.0, 0])
644 extraSizeMax = numpy.array([6.0, 6.0, 0])
646 return extraSizeMin, extraSizeMax
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)
664 self.view3D = self.parent.alwaysAutoPlace
668 self.zoom = self.parent.machineSize[0] / 2 + 10
669 self.allowDrag = False
671 self.objColor = profile.getPreferenceColour('model_colour')
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]))
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)
688 def OnMouseMotion(self,e):
689 if self.allowDrag and e.Dragging() and e.LeftIsDown():
691 self.yaw += e.GetX() - self.oldX
692 self.pitch -= e.GetY() - self.oldY
697 elif not self.parent.alwaysAutoPlace:
698 item = self.parent.selection
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
705 self.allowDrag = False
706 if e.Dragging() and e.RightIsDown():
708 self.zoom += e.GetY() - self.oldY
715 def OnMouseWheel(self,e):
717 self.zoom *= 1.0 - float(e.GetWheelRotation() / e.GetWheelDelta()) / 10.0
722 def OnEraseBackground(self,event):
723 #Workaround for windows background redraw flicker.
726 def OnSize(self,event):
729 def OnPaint(self,event):
730 dc = wx.PaintDC(self)
731 self.SetCurrent(self.context)
732 opengl.InitGL(self, self.view3D, self.zoom)
734 glTranslate(0,0,-self.zoom)
735 glRotate(-self.pitch, 1,0,0)
736 glRotate(self.yaw, 0,0,1)
738 glTranslate(self.offsetX, self.offsetY, 0.0)
739 glTranslate(-self.parent.machineSize[0]/2, -self.parent.machineSize[1]/2, 0)
741 self.viewport = glGetIntegerv(GL_VIEWPORT);
742 self.modelMatrix = glGetDoublev(GL_MODELVIEW_MATRIX);
743 self.projMatrix = glGetDoublev(GL_PROJECTION_MATRIX);
749 machineSize = self.parent.machineSize
750 extraSizeMin, extraSizeMax = self.parent.getExtraHeadSize()
752 for item in self.parent.list:
753 item.validPlacement = True
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
773 for item in self.parent.list:
774 if item == self.parent.selection:
776 if item.modelDisplayList is None:
777 item.modelDisplayList = glGenLists(1);
779 item.modelDirty = False
780 modelSize = item.getMaximum() - item.getMinimum()
781 glNewList(item.modelDisplayList, GL_COMPILE)
782 opengl.DrawMesh(item.mesh)
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))
790 glLightfv(GL_LIGHT0, GL_DIFFUSE, self.objColor)
791 glLightfv(GL_LIGHT0, GL_AMBIENT, map(lambda x: x / 2, self.objColor))
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])
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])
801 glEnable(GL_LIGHTING)
802 glTranslate(item.centerX, item.centerY, 0)
804 glScalef(item.scale, item.scale, item.scale)
805 glCallList(item.modelDisplayList)
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]
813 glDisable(GL_LIGHTING)
815 if not self.parent.alwaysAutoPlace:
816 if self.parent.selection == item:
818 glColor3f(1.0,0.0,0.3)
820 glColor3f(1.0,0.0,1.0)
821 opengl.DrawBox(vMin, vMax)
823 glColor3f(1.0,0.3,0.0)
825 glColor3f(1.0,1.0,0.0)
826 opengl.DrawBox(vMinHead, vMaxHead)
829 glColor3f(0.5,0.0,0.1)
831 glColor3f(0.5,0.0,0.5)
832 opengl.DrawBox(vMinHead, vMaxHead)
835 glColor3f(0.7,0.1,0.0)
837 glColor3f(0.7,0.7,0.0)
838 opengl.DrawBox(vMin, vMax)
842 opengl.DrawMachine(util3d.Vector3(machineSize[0], machineSize[1], machineSize[2]))
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))
850 self.sliceCommand = sliceCommand
851 self.resultFilename = resultFilename
852 self.fileCount = fileCount
854 self.prevStep = 'start'
855 self.totalDoneFactor = 0.0
856 self.startTime = time.time()
857 self.sliceStartTime = time.time()
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)
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)
875 self.Bind(wx.EVT_BUTTON, self.OnAbort, self.abortButton)
876 self.SetSizer(self.sizer)
880 threading.Thread(target=self.OnRun).start()
882 def OnAbort(self, e):
887 self.abortButton.SetLabel('Close')
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
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)
906 self.progressLog = []
907 p = sliceRun.startSliceCommandProcess(self.sliceCommand)
908 line = p.stdout.readline()
909 while(len(line) > 0):
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)
917 self.progressLog.append(line)
918 wx.CallAfter(self.statusText.SetLabel, line)
921 wx.CallAfter(self.statusText.SetLabel, "Aborted by user.")
923 line = p.stdout.readline()
924 line = p.stderr.readline()
927 self.progressLog.append(line)
928 line = p.stderr.readline()
929 self.returnCode = p.wait()
930 self.progressGauge2.SetValue(self.fileCount)
932 gcode = gcodeInterpreter.gcode()
933 gcode.load(self.resultFilename)
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()
943 status += "\nCost: %s" % (cost)
944 profile.replaceGCodeTags(self.resultFilename, gcode)
945 wx.CallAfter(self.statusText.SetLabel, status)
946 wx.CallAfter(self.OnSliceDone)
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'
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)
973 taskbar.setBusy(self, False)
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))
981 def OnOpenFileLocation(self, e):
982 explorer.openExplorer(self.resultFilename)
984 def OnPrint(self, e):
985 printWindow.printFile(self.resultFilename)
987 def OnShowLog(self, e):
988 LogWindow('\n'.join(self.progressLog))
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)
995 wx.EVT_CLOSE(self, self.OnClose)
997 self.panel = configBase.configPanelBase(self)
998 extruderAmount = int(profile.getPreference('extruder_amount'))
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)
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)
1019 self.MakeModal(True)
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()
1028 self.MakeModal(False)
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))