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 *
20 print "Failed to find PyOpenGL: http://pyopengl.sourceforge.net/"
23 from Cura.gui.util import opengl
24 from Cura.gui.util import toolbarUtil
25 from Cura.gui import configBase
26 from Cura.gui import printWindow
27 from Cura.gui.util import dropTarget
28 from Cura.gui.util import taskbar
29 from Cura.util import validators
30 from Cura.util import profile
31 from Cura.util import util3d
32 from Cura.util import meshLoader
33 from Cura.util import stl
34 from Cura.util import mesh
35 from Cura.util import sliceRun
36 from Cura.util import gcodeInterpreter
37 from Cura.util import exporer
42 class ProjectObject(object):
43 def __init__(self, parent, filename):
44 super(ProjectObject, self).__init__()
46 self.mesh = meshLoader.loadMesh(filename)
49 self.filename = filename
60 self.modelDisplayList = None
61 self.modelDirty = False
63 self.mesh.getMinimumZ()
65 self.centerX = -self.getMinimum()[0] + 5
66 self.centerY = -self.getMinimum()[1] + 5
68 self.updateModelTransform()
70 self.centerX = -self.getMinimum()[0] + 5
71 self.centerY = -self.getMinimum()[1] + 5
73 def isSameExceptForPosition(self, other):
74 if self.filename != other.filename:
76 if self.scale != other.scale:
78 if self.rotate != other.rotate:
80 if self.flipX != other.flipX:
82 if self.flipY != other.flipY:
84 if self.flipZ != other.flipZ:
86 if self.swapXZ != other.swapXZ:
88 if self.swapYZ != other.swapYZ:
90 if self.extruder != other.extruder:
92 if self.profile != other.profile:
96 def updateModelTransform(self):
97 self.mesh.setRotateMirror(self.rotate, self.flipX, self.flipY, self.flipZ, self.swapXZ, self.swapYZ)
98 minZ = self.mesh.getMinimumZ()
99 minV = self.getMinimum()
100 maxV = self.getMaximum()
101 self.mesh.vertexes -= numpy.array([minV[0] + (maxV[0] - minV[0]) / 2, minV[1] + (maxV[1] - minV[1]) / 2, minZ])
102 minZ = self.mesh.getMinimumZ()
103 self.modelDirty = True
105 def getMinimum(self):
106 return self.mesh.getMinimum()
107 def getMaximum(self):
108 return self.mesh.getMaximum()
110 return self.mesh.getSize()
113 p = ProjectObject(self.parent, self.filename)
115 p.centerX = self.centerX + 5
116 p.centerY = self.centerY + 5
118 p.filename = self.filename
120 p.rotate = self.rotate
124 p.swapXZ = self.swapXZ
125 p.swapYZ = self.swapYZ
126 p.extruder = self.extruder
127 p.profile = self.profile
129 p.updateModelTransform()
134 if self.centerX < -self.getMinimum()[0] * self.scale + self.parent.extruderOffset[self.extruder][0]:
135 self.centerX = -self.getMinimum()[0] * self.scale + self.parent.extruderOffset[self.extruder][0]
136 if self.centerY < -self.getMinimum()[1] * self.scale + self.parent.extruderOffset[self.extruder][1]:
137 self.centerY = -self.getMinimum()[1] * self.scale + self.parent.extruderOffset[self.extruder][1]
138 if self.centerX > self.parent.machineSize[0] + self.parent.extruderOffset[self.extruder][0] - self.getMaximum()[0] * self.scale:
139 self.centerX = self.parent.machineSize[0] + self.parent.extruderOffset[self.extruder][0] - self.getMaximum()[0] * self.scale
140 if self.centerY > self.parent.machineSize[1] + self.parent.extruderOffset[self.extruder][1] - self.getMaximum()[1] * self.scale:
141 self.centerY = self.parent.machineSize[1] + self.parent.extruderOffset[self.extruder][1] - self.getMaximum()[1] * self.scale
143 class projectPlanner(wx.Frame):
144 "Main user interface window"
146 super(projectPlanner, self).__init__(None, title='Cura - Project Planner')
148 wx.EVT_CLOSE(self, self.OnClose)
149 self.panel = wx.Panel(self, -1)
150 self.SetSizer(wx.BoxSizer(wx.VERTICAL))
151 self.GetSizer().Add(self.panel, 1, flag=wx.EXPAND)
153 self.SetDropTarget(dropTarget.FileDropTarget(self.OnDropFiles, meshLoader.supportedExtensions()))
156 self.selection = None
158 self.alwaysAutoPlace = profile.getPreference('planner_always_autoplace') == 'True'
160 self.machineSize = numpy.array([profile.getPreferenceFloat('machine_width'), profile.getPreferenceFloat('machine_depth'), profile.getPreferenceFloat('machine_height')])
161 self.headSizeMin = numpy.array([profile.getPreferenceFloat('extruder_head_size_min_x'), profile.getPreferenceFloat('extruder_head_size_min_y'),0])
162 self.headSizeMax = numpy.array([profile.getPreferenceFloat('extruder_head_size_max_x'), profile.getPreferenceFloat('extruder_head_size_max_y'),0])
164 self.extruderOffset = [
165 numpy.array([0,0,0]),
166 numpy.array([profile.getPreferenceFloat('extruder_offset_x1'), profile.getPreferenceFloat('extruder_offset_y1'), 0]),
167 numpy.array([profile.getPreferenceFloat('extruder_offset_x2'), profile.getPreferenceFloat('extruder_offset_y2'), 0]),
168 numpy.array([profile.getPreferenceFloat('extruder_offset_x3'), profile.getPreferenceFloat('extruder_offset_y3'), 0])]
170 self.toolbar = toolbarUtil.Toolbar(self.panel)
172 toolbarUtil.NormalButton(self.toolbar, self.OnLoadProject, 'open.png', 'Open project')
173 toolbarUtil.NormalButton(self.toolbar, self.OnSaveProject, 'save.png', 'Save project')
174 self.toolbar.AddSeparator()
176 toolbarUtil.RadioButton(self.toolbar, group, 'object-3d-on.png', 'object-3d-off.png', '3D view', callback=self.On3DClick).SetValue(self.alwaysAutoPlace)
177 toolbarUtil.RadioButton(self.toolbar, group, 'object-top-on.png', 'object-top-off.png', 'Topdown view', callback=self.OnTopClick).SetValue(not self.alwaysAutoPlace)
178 self.toolbar.AddSeparator()
179 toolbarUtil.NormalButton(self.toolbar, self.OnPreferences, 'preferences.png', 'Project planner preferences')
180 self.toolbar.AddSeparator()
181 toolbarUtil.NormalButton(self.toolbar, self.OnCutMesh, 'cut-mesh.png', 'Cut a plate STL into multiple STL files, and add those files to the project.\nNote: Splitting up plates sometimes takes a few minutes.')
182 toolbarUtil.NormalButton(self.toolbar, self.OnSaveCombinedSTL, 'save-combination.png', 'Save all the combined STL files into a single STL file as a plate.')
183 self.toolbar.AddSeparator()
185 self.printOneAtATime = toolbarUtil.RadioButton(self.toolbar, group, 'view-normal-on.png', 'view-normal-off.png', 'Print one object at a time', callback=self.OnPrintTypeChange)
186 self.printAllAtOnce = toolbarUtil.RadioButton(self.toolbar, group, 'all-at-once-on.png', 'all-at-once-off.png', 'Print all the objects at once', callback=self.OnPrintTypeChange)
187 self.toolbar.AddSeparator()
188 toolbarUtil.NormalButton(self.toolbar, self.OnQuit, 'exit.png', 'Close project planner')
190 self.toolbar.Realize()
192 self.toolbar2 = toolbarUtil.Toolbar(self.panel)
194 toolbarUtil.NormalButton(self.toolbar2, self.OnAddModel, 'object-add.png', 'Add model')
195 toolbarUtil.NormalButton(self.toolbar2, self.OnRemModel, 'object-remove.png', 'Remove model')
196 self.toolbar2.AddSeparator()
197 toolbarUtil.NormalButton(self.toolbar2, self.OnMoveUp, 'move-up.png', 'Move model up in print list')
198 toolbarUtil.NormalButton(self.toolbar2, self.OnMoveDown, 'move-down.png', 'Move model down in print list')
199 toolbarUtil.NormalButton(self.toolbar2, self.OnCopy, 'copy.png', 'Make a copy of the current selected object')
200 toolbarUtil.NormalButton(self.toolbar2, self.OnSetCustomProfile, 'set-profile.png', 'Set a custom profile to be used to prepare a specific object.')
201 self.toolbar2.AddSeparator()
202 if not self.alwaysAutoPlace:
203 toolbarUtil.NormalButton(self.toolbar2, self.OnAutoPlace, 'autoplace.png', 'Automaticly organize the objects on the platform.')
204 toolbarUtil.NormalButton(self.toolbar2, self.OnSlice, 'slice.png', 'Prepare to project into a gcode file.')
205 self.toolbar2.Realize()
207 self.toolbar3 = toolbarUtil.Toolbar(self.panel)
208 self.mirrorX = toolbarUtil.ToggleButton(self.toolbar3, 'flip_x', 'object-mirror-x-on.png', 'object-mirror-x-off.png', 'Mirror X', callback=self.OnMirrorChange)
209 self.mirrorY = toolbarUtil.ToggleButton(self.toolbar3, 'flip_y', 'object-mirror-y-on.png', 'object-mirror-y-off.png', 'Mirror Y', callback=self.OnMirrorChange)
210 self.mirrorZ = toolbarUtil.ToggleButton(self.toolbar3, 'flip_z', 'object-mirror-z-on.png', 'object-mirror-z-off.png', 'Mirror Z', callback=self.OnMirrorChange)
211 self.toolbar3.AddSeparator()
214 self.swapXZ = toolbarUtil.ToggleButton(self.toolbar3, 'swap_xz', 'object-swap-xz-on.png', 'object-swap-xz-off.png', 'Swap XZ', callback=self.OnMirrorChange)
215 self.swapYZ = toolbarUtil.ToggleButton(self.toolbar3, 'swap_yz', 'object-swap-yz-on.png', 'object-swap-yz-off.png', 'Swap YZ', callback=self.OnMirrorChange)
216 self.toolbar3.Realize()
218 sizer = wx.GridBagSizer(2,2)
219 self.panel.SetSizer(sizer)
220 self.preview = PreviewGLCanvas(self.panel, self)
221 self.listbox = wx.ListBox(self.panel, -1, choices=[])
222 self.addButton = wx.Button(self.panel, -1, "Add")
223 self.remButton = wx.Button(self.panel, -1, "Remove")
224 self.sliceButton = wx.Button(self.panel, -1, "Prepare")
225 if not self.alwaysAutoPlace:
226 self.autoPlaceButton = wx.Button(self.panel, -1, "Auto Place")
228 sizer.Add(self.toolbar, (0,0), span=(1,1), flag=wx.EXPAND|wx.LEFT|wx.RIGHT)
229 sizer.Add(self.toolbar2, (0,1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.RIGHT)
230 sizer.Add(self.preview, (1,0), span=(5,1), flag=wx.EXPAND)
231 sizer.Add(self.listbox, (1,1), span=(1,2), flag=wx.EXPAND)
232 sizer.Add(self.toolbar3, (2,1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.RIGHT)
233 sizer.Add(self.addButton, (3,1), span=(1,1))
234 sizer.Add(self.remButton, (3,2), span=(1,1))
235 sizer.Add(self.sliceButton, (4,1), span=(1,1))
236 if not self.alwaysAutoPlace:
237 sizer.Add(self.autoPlaceButton, (4,2), span=(1,1))
238 sizer.AddGrowableCol(0)
239 sizer.AddGrowableRow(1)
241 self.addButton.Bind(wx.EVT_BUTTON, self.OnAddModel)
242 self.remButton.Bind(wx.EVT_BUTTON, self.OnRemModel)
243 self.sliceButton.Bind(wx.EVT_BUTTON, self.OnSlice)
244 if not self.alwaysAutoPlace:
245 self.autoPlaceButton.Bind(wx.EVT_BUTTON, self.OnAutoPlace)
246 self.listbox.Bind(wx.EVT_LISTBOX, self.OnListSelect)
248 panel = wx.Panel(self.panel, -1)
249 sizer.Add(panel, (5,1), span=(1,2))
251 sizer = wx.GridBagSizer(2,2)
252 panel.SetSizer(sizer)
254 self.scaleCtrl = wx.TextCtrl(panel, -1, '')
255 self.rotateCtrl = wx.SpinCtrl(panel, -1, '', size=(21*4,21), style=wx.SP_ARROW_KEYS)
256 self.rotateCtrl.SetRange(0, 360)
258 sizer.Add(wx.StaticText(panel, -1, 'Scale'), (0,0), flag=wx.ALIGN_CENTER_VERTICAL)
259 sizer.Add(self.scaleCtrl, (0,1), flag=wx.ALIGN_BOTTOM|wx.EXPAND)
260 sizer.Add(wx.StaticText(panel, -1, 'Rotate'), (1,0), flag=wx.ALIGN_CENTER_VERTICAL)
261 sizer.Add(self.rotateCtrl, (1,1), flag=wx.ALIGN_BOTTOM|wx.EXPAND)
263 if int(profile.getPreference('extruder_amount')) > 1:
264 self.extruderCtrl = wx.ComboBox(panel, -1, '1', choices=map(str, range(1, int(profile.getPreference('extruder_amount'))+1)), style=wx.CB_DROPDOWN|wx.CB_READONLY)
265 sizer.Add(wx.StaticText(panel, -1, 'Extruder'), (2,0), flag=wx.ALIGN_CENTER_VERTICAL)
266 sizer.Add(self.extruderCtrl, (2,1), flag=wx.ALIGN_BOTTOM|wx.EXPAND)
267 self.extruderCtrl.Bind(wx.EVT_COMBOBOX, self.OnExtruderChange)
269 self.scaleCtrl.Bind(wx.EVT_TEXT, self.OnScaleChange)
270 self.rotateCtrl.Bind(wx.EVT_SPINCTRL, self.OnRotateChange)
272 self.SetSize((800,600))
274 def OnClose(self, e):
280 def OnPreferences(self, e):
281 prefDialog = preferencesDialog(self)
283 prefDialog.Show(True)
285 def OnCutMesh(self, e):
286 dlg=wx.FileDialog(self, "Open file to cut", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
287 dlg.SetWildcard(meshLoader.wildcardFilter())
288 if dlg.ShowModal() == wx.ID_OK:
289 filename = dlg.GetPath()
290 model = meshLoader.loadMesh(filename)
291 pd = wx.ProgressDialog('Splitting model.', 'Splitting model into multiple parts.', model.vertexCount, self, wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME | wx.PD_SMOOTH)
292 parts = model.splitToParts(pd.Update)
294 partFilename = filename[:filename.rfind('.')] + "_part%d.stl" % (parts.index(part))
295 stl.saveAsSTL(part, partFilename)
296 item = ProjectObject(self, partFilename)
297 self.list.append(item)
298 self.selection = item
299 self._updateListbox()
300 self.OnListSelect(None)
302 self.preview.Refresh()
305 def OnDropFiles(self, filenames):
306 for filename in filenames:
307 item = ProjectObject(self, filename)
308 profile.putPreference('lastFile', item.filename)
309 self.list.append(item)
310 self.selection = item
311 self._updateListbox()
312 self.OnListSelect(None)
313 self.preview.Refresh()
315 def OnPrintTypeChange(self):
317 if self.printAllAtOnce.GetValue():
319 if self.alwaysAutoPlace:
320 self.OnAutoPlace(None)
321 self.preview.Refresh()
323 def OnSaveCombinedSTL(self, e):
324 dlg=wx.FileDialog(self, "Save as STL", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
325 dlg.SetWildcard("STL files (*.stl)|*.stl;*.STL")
326 if dlg.ShowModal() == wx.ID_OK:
327 self._saveCombinedSTL(dlg.GetPath())
330 def _saveCombinedSTL(self, filename):
332 for item in self.list:
333 totalCount += item.mesh.vertexCount
335 output._prepareVertexCount(totalCount)
336 for item in self.list:
337 offset = numpy.array([item.centerX, item.centerY, 0])
338 for v in item.mesh.vertexes:
339 v0 = v * item.scale + offset
340 output.addVertex(v0[0], v0[1], v0[2])
341 stl.saveAsSTL(output, filename)
343 def OnSaveProject(self, e):
344 dlg=wx.FileDialog(self, "Save project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
345 dlg.SetWildcard("Project files (*.curaproject)|*.curaproject")
346 if dlg.ShowModal() == wx.ID_OK:
347 cp = ConfigParser.ConfigParser()
349 for item in self.list:
350 section = 'model_%d' % (i)
351 cp.add_section(section)
352 cp.set(section, 'filename', item.filename.encode("utf-8"))
353 cp.set(section, 'centerX', str(item.centerX))
354 cp.set(section, 'centerY', str(item.centerY))
355 cp.set(section, 'scale', str(item.scale))
356 cp.set(section, 'rotate', str(item.rotate))
357 cp.set(section, 'flipX', str(item.flipX))
358 cp.set(section, 'flipY', str(item.flipY))
359 cp.set(section, 'flipZ', str(item.flipZ))
360 cp.set(section, 'swapXZ', str(item.swapXZ))
361 cp.set(section, 'swapYZ', str(item.swapYZ))
362 cp.set(section, 'extruder', str(item.extruder+1))
363 if item.profile != None:
364 cp.set(section, 'profile', item.profile)
366 cp.write(open(dlg.GetPath(), "w"))
369 def OnLoadProject(self, e):
370 dlg=wx.FileDialog(self, "Open project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
371 dlg.SetWildcard("Project files (*.curaproject)|*.curaproject")
372 if dlg.ShowModal() == wx.ID_OK:
373 cp = ConfigParser.ConfigParser()
374 cp.read(dlg.GetPath())
377 while cp.has_section('model_%d' % (i)):
378 section = 'model_%d' % (i)
380 item = ProjectObject(self, unicode(cp.get(section, 'filename'), "utf-8"))
381 item.centerX = float(cp.get(section, 'centerX'))
382 item.centerY = float(cp.get(section, 'centerY'))
383 item.scale = float(cp.get(section, 'scale'))
384 item.rotate = float(cp.get(section, 'rotate'))
385 item.flipX = cp.get(section, 'flipX') == 'True'
386 item.flipY = cp.get(section, 'flipY') == 'True'
387 item.flipZ = cp.get(section, 'flipZ') == 'True'
388 item.swapXZ = cp.get(section, 'swapXZ') == 'True'
389 item.swapYZ = cp.get(section, 'swapYZ') == 'True'
390 if cp.has_option(section, 'extruder'):
391 item.extuder = int(cp.get(section, 'extruder')) - 1
392 if cp.has_option(section, 'profile'):
393 item.profile = cp.get(section, 'profile')
394 item.updateModelTransform()
397 self.list.append(item)
399 self.selected = self.list[0]
400 self._updateListbox()
401 self.OnListSelect(None)
402 self.preview.Refresh()
407 self.preview.yaw = 30
408 self.preview.pitch = 60
409 self.preview.zoom = 300
410 self.preview.view3D = True
411 self.preview.Refresh()
413 def OnTopClick(self):
414 self.preview.view3D = False
415 self.preview.zoom = self.machineSize[0] / 2 + 10
416 self.preview.offsetX = 0
417 self.preview.offsetY = 0
418 self.preview.Refresh()
420 def OnListSelect(self, e):
421 if self.listbox.GetSelection() == -1:
423 self.selection = self.list[self.listbox.GetSelection()]
424 self.scaleCtrl.SetValue(str(self.selection.scale))
425 self.rotateCtrl.SetValue(int(self.selection.rotate))
426 if int(profile.getPreference('extruder_amount')) > 1:
427 self.extruderCtrl.SetValue(str(self.selection.extruder+1))
428 self.mirrorX.SetValue(self.selection.flipX)
429 self.mirrorY.SetValue(self.selection.flipY)
430 self.mirrorZ.SetValue(self.selection.flipZ)
431 self.swapXZ.SetValue(self.selection.swapXZ)
432 self.swapYZ.SetValue(self.selection.swapYZ)
434 self.preview.Refresh()
436 def OnAddModel(self, e):
437 dlg=wx.FileDialog(self, "Open file to print", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE)
438 dlg.SetWildcard(meshLoader.wildcardFilter())
439 if dlg.ShowModal() == wx.ID_OK:
440 for filename in dlg.GetPaths():
441 item = ProjectObject(self, filename)
442 profile.putPreference('lastFile', item.filename)
443 self.list.append(item)
444 self.selection = item
445 self._updateListbox()
446 self.OnListSelect(None)
447 self.preview.Refresh()
450 def OnRemModel(self, e):
451 if self.selection is None:
453 self.list.remove(self.selection)
454 self._updateListbox()
455 self.preview.Refresh()
457 def OnMoveUp(self, e):
458 if self.selection is None:
460 i = self.listbox.GetSelection()
463 self.list.remove(self.selection)
464 self.list.insert(i-1, self.selection)
465 self._updateListbox()
466 self.preview.Refresh()
468 def OnMoveDown(self, e):
469 if self.selection is None:
471 i = self.listbox.GetSelection()
472 if i == len(self.list) - 1:
474 self.list.remove(self.selection)
475 self.list.insert(i+1, self.selection)
476 self._updateListbox()
477 self.preview.Refresh()
480 if self.selection is None:
483 item = self.selection.clone()
484 self.list.insert(self.list.index(self.selection), item)
485 self.selection = item
487 self._updateListbox()
488 self.preview.Refresh()
490 def OnSetCustomProfile(self, e):
491 if self.selection is None:
494 dlg=wx.FileDialog(self, "Select profile", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
495 dlg.SetWildcard("Profile files (*.ini)|*.ini;*.INI")
496 if dlg.ShowModal() == wx.ID_OK:
497 self.selection.profile = dlg.GetPath()
499 self.selection.profile = None
500 self._updateListbox()
503 def _updateListbox(self):
505 for item in self.list:
506 if item.profile is not None:
507 self.listbox.AppendAndEnsureVisible(os.path.split(item.filename)[1] + " *")
509 self.listbox.AppendAndEnsureVisible(os.path.split(item.filename)[1])
510 if self.selection in self.list:
511 self.listbox.SetSelection(self.list.index(self.selection))
512 elif len(self.list) > 0:
513 self.selection = self.list[0]
514 self.listbox.SetSelection(0)
516 self.selection = None
517 self.listbox.SetSelection(-1)
518 if self.alwaysAutoPlace:
519 self.OnAutoPlace(None)
521 def OnAutoPlace(self, e):
522 bestAllowedSize = int(self.machineSize[1])
523 bestArea = self._doAutoPlace(bestAllowedSize)
524 for i in xrange(10, int(self.machineSize[1]), 10):
525 area = self._doAutoPlace(i)
529 self._doAutoPlace(bestAllowedSize)
530 if not self.alwaysAutoPlace:
531 for item in self.list:
533 self.preview.Refresh()
535 def _doAutoPlace(self, allowedSizeY):
536 extraSizeMin, extraSizeMax = self.getExtraHeadSize()
538 if extraSizeMin[0] > extraSizeMax[0]:
539 posX = self.machineSize[0]
547 minX = self.machineSize[0]
548 minY = self.machineSize[1]
551 for item in self.list:
552 item.centerX = posX + item.getMaximum()[0] * item.scale * dirX
553 item.centerY = posY + item.getMaximum()[1] * item.scale * dirY
554 if item.centerY + item.getSize()[1] >= allowedSizeY:
556 posX = minX - extraSizeMax[0] - 1
558 posX = maxX + extraSizeMin[0] + 1
560 item.centerX = posX + item.getMaximum()[0] * item.scale * dirX
561 item.centerY = posY + item.getMaximum()[1] * item.scale * dirY
562 posY += item.getSize()[1] * item.scale * dirY + extraSizeMin[1] + 1
563 minX = min(minX, item.centerX - item.getSize()[0] * item.scale / 2)
564 minY = min(minY, item.centerY - item.getSize()[1] * item.scale / 2)
565 maxX = max(maxX, item.centerX + item.getSize()[0] * item.scale / 2)
566 maxY = max(maxY, item.centerY + item.getSize()[1] * item.scale / 2)
568 for item in self.list:
570 item.centerX -= minX / 2
572 item.centerX += (self.machineSize[0] - maxX) / 2
573 item.centerY += (self.machineSize[1] - maxY) / 2
575 if minX < 0 or maxX > self.machineSize[0]:
576 return ((maxX - minX) + (maxY - minY)) * 100
578 return (maxX - minX) + (maxY - minY)
580 def OnSlice(self, e):
581 dlg=wx.FileDialog(self, "Save project gcode file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
582 dlg.SetWildcard("GCode file (*.gcode)|*.gcode")
583 if dlg.ShowModal() != wx.ID_OK:
586 resultFilename = dlg.GetPath()
589 put = profile.setTempOverride
590 oldProfile = profile.getGlobalProfileString()
592 if self.printMode == 0:
595 for item in self.list:
596 fileList.append(item.filename)
597 if profile.getPreference('machine_center_is_zero') == 'True':
598 pos = [item.centerX - self.machineSize[0] / 2, item.centerY - self.machineSize[1] / 2]
600 pos = [item.centerX, item.centerY]
601 positionList.append(pos + (item.mesh.matrix * item.scale).reshape((9,)).tolist())
602 sliceCommand = sliceRun.getSliceCommand(resultFilename, fileList, positionList)
604 self._saveCombinedSTL(resultFilename + "_temp_.stl")
605 sliceCommand = sliceRun.getSliceCommand(resultFilename, [resultFilename + "_temp_.stl"], [profile.getMachineCenterCoords()])
607 pspw = ProjectSliceProgressWindow(sliceCommand, resultFilename, len(self.list))
611 def OnScaleChange(self, e):
612 if self.selection is None:
615 self.selection.scale = float(self.scaleCtrl.GetValue())
617 self.selection.scale = 1.0
618 if self.alwaysAutoPlace:
619 self.OnAutoPlace(None)
620 self.preview.Refresh()
622 def OnRotateChange(self, e):
623 if self.selection is None:
625 self.selection.rotate = float(self.rotateCtrl.GetValue())
626 self.selection.updateModelTransform()
627 if self.alwaysAutoPlace:
628 self.OnAutoPlace(None)
629 self.preview.Refresh()
631 def OnExtruderChange(self, e):
632 if self.selection is None:
634 self.selection.extruder = int(self.extruderCtrl.GetValue()) - 1
635 self.preview.Refresh()
637 def OnMirrorChange(self):
638 if self.selection is None:
640 self.selection.flipX = self.mirrorX.GetValue()
641 self.selection.flipY = self.mirrorY.GetValue()
642 self.selection.flipZ = self.mirrorZ.GetValue()
643 self.selection.swapXZ = self.swapXZ.GetValue()
644 self.selection.swapYZ = self.swapYZ.GetValue()
645 self.selection.updateModelTransform()
646 if self.alwaysAutoPlace:
647 self.OnAutoPlace(None)
648 self.preview.Refresh()
650 def getExtraHeadSize(self):
651 extraSizeMin = self.headSizeMin
652 extraSizeMax = self.headSizeMax
653 if profile.getProfileSettingFloat('skirt_line_count') > 0:
654 skirtSize = profile.getProfileSettingFloat('skirt_line_count') * profile.calculateEdgeWidth() + profile.getProfileSettingFloat('skirt_gap')
655 extraSizeMin = extraSizeMin + numpy.array([skirtSize, skirtSize, 0])
656 extraSizeMax = extraSizeMax + numpy.array([skirtSize, skirtSize, 0])
657 if profile.getProfileSetting('enable_raft') != 'False':
658 raftSize = profile.getProfileSettingFloat('raft_margin') * 2
659 extraSizeMin = extraSizeMin + numpy.array([raftSize, raftSize, 0])
660 extraSizeMax = extraSizeMax + numpy.array([raftSize, raftSize, 0])
661 if profile.getProfileSetting('support') != 'None':
662 extraSizeMin = extraSizeMin + numpy.array([3.0, 0, 0])
663 extraSizeMax = extraSizeMax + numpy.array([3.0, 0, 0])
665 if self.printMode == 1:
666 extraSizeMin = numpy.array([6.0, 6.0, 0])
667 extraSizeMax = numpy.array([6.0, 6.0, 0])
669 return extraSizeMin, extraSizeMax
671 class PreviewGLCanvas(glcanvas.GLCanvas):
672 def __init__(self, parent, projectPlannerWindow):
673 attribList = (glcanvas.WX_GL_RGBA, glcanvas.WX_GL_DOUBLEBUFFER, glcanvas.WX_GL_DEPTH_SIZE, 24, glcanvas.WX_GL_STENCIL_SIZE, 8)
674 glcanvas.GLCanvas.__init__(self, parent, attribList = attribList)
675 self.parent = projectPlannerWindow
676 self.context = glcanvas.GLContext(self)
677 wx.EVT_PAINT(self, self.OnPaint)
678 wx.EVT_SIZE(self, self.OnSize)
679 wx.EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)
680 wx.EVT_LEFT_DOWN(self, self.OnMouseLeftDown)
681 wx.EVT_MOTION(self, self.OnMouseMotion)
682 wx.EVT_MOUSEWHEEL(self, self.OnMouseWheel)
687 self.view3D = self.parent.alwaysAutoPlace
691 self.zoom = self.parent.machineSize[0] / 2 + 10
692 self.allowDrag = False
694 self.objColor = profile.getPreferenceColour('model_colour')
696 def OnMouseLeftDown(self,e):
697 self.allowDrag = True
698 if not self.parent.alwaysAutoPlace and not self.view3D:
699 p0 = numpy.array(gluUnProject(e.GetX(), self.viewport[1] + self.viewport[3] - e.GetY(), 0, self.modelMatrix, self.projMatrix, self.viewport))
700 p1 = numpy.array(gluUnProject(e.GetX(), self.viewport[1] + self.viewport[3] - e.GetY(), 1, self.modelMatrix, self.projMatrix, self.viewport))
701 cursorZ0 = p0 - (p1 - p0) * (p0[2] / (p1[2] - p0[2]))
703 for item in self.parent.list:
704 iMin = (item.getMinimum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) - self.parent.extruderOffset[item.extruder]
705 iMax = (item.getMaximum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) - self.parent.extruderOffset[item.extruder]
706 if iMin[0] <= cursorZ0[0] <= iMax[0] and iMin[1] <= cursorZ0[1] <= iMax[1]:
707 self.parent.selection = item
708 self.parent._updateListbox()
709 self.parent.OnListSelect(None)
711 def OnMouseMotion(self,e):
712 if self.allowDrag and e.Dragging() and e.LeftIsDown():
714 self.yaw += e.GetX() - self.oldX
715 self.pitch -= e.GetY() - self.oldY
720 elif not self.parent.alwaysAutoPlace:
721 item = self.parent.selection
723 item.centerX += float(e.GetX() - self.oldX) * self.zoom / self.GetSize().GetHeight() * 2
724 item.centerY -= float(e.GetY() - self.oldY) * self.zoom / self.GetSize().GetHeight() * 2
728 self.allowDrag = False
729 if e.Dragging() and e.RightIsDown():
731 self.zoom += e.GetY() - self.oldY
738 def OnMouseWheel(self,e):
740 self.zoom *= 1.0 - float(e.GetWheelRotation() / e.GetWheelDelta()) / 10.0
745 def OnEraseBackground(self,event):
746 #Workaround for windows background redraw flicker.
749 def OnSize(self,event):
752 def OnPaint(self,event):
753 dc = wx.PaintDC(self)
754 if not hasOpenGLlibs:
756 dc.DrawText("No PyOpenGL installation found.\nNo preview window available.", 10, 10)
758 self.SetCurrent(self.context)
759 opengl.InitGL(self, self.view3D, self.zoom)
761 glTranslate(0,0,-self.zoom)
762 glRotate(-self.pitch, 1,0,0)
763 glRotate(self.yaw, 0,0,1)
765 glTranslate(self.offsetX, self.offsetY, 0.0)
766 glTranslate(-self.parent.machineSize[0]/2, -self.parent.machineSize[1]/2, 0)
768 self.viewport = glGetIntegerv(GL_VIEWPORT);
769 self.modelMatrix = glGetDoublev(GL_MODELVIEW_MATRIX);
770 self.projMatrix = glGetDoublev(GL_PROJECTION_MATRIX);
776 machineSize = self.parent.machineSize
777 extraSizeMin, extraSizeMax = self.parent.getExtraHeadSize()
779 for item in self.parent.list:
780 item.validPlacement = True
783 for idx1 in xrange(0, len(self.parent.list)):
784 item = self.parent.list[idx1]
785 iMin1 = (item.getMinimum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) - extraSizeMin - self.parent.extruderOffset[item.extruder]
786 iMax1 = (item.getMaximum() * item.scale) + numpy.array([item.centerX, item.centerY, 0]) + extraSizeMax - self.parent.extruderOffset[item.extruder]
787 if iMin1[0] < -self.parent.headSizeMin[0] or iMin1[1] < -self.parent.headSizeMin[1]:
788 item.validPlacement = False
789 if iMax1[0] > machineSize[0] + self.parent.headSizeMax[0] or iMax1[1] > machineSize[1] + self.parent.headSizeMax[1]:
790 item.validPlacement = False
791 for idx2 in xrange(0, idx1):
792 item2 = self.parent.list[idx2]
793 iMin2 = (item2.getMinimum() * item2.scale) + numpy.array([item2.centerX, item2.centerY, 0])
794 iMax2 = (item2.getMaximum() * item2.scale) + numpy.array([item2.centerX, item2.centerY, 0])
795 if item != item2 and iMax1[0] >= iMin2[0] and iMin1[0] <= iMax2[0] and iMax1[1] >= iMin2[1] and iMin1[1] <= iMax2[1]:
796 item.validPlacement = False
800 for item in self.parent.list:
801 if item == self.parent.selection:
803 if item.modelDisplayList is None:
804 item.modelDisplayList = glGenLists(1);
806 item.modelDirty = False
807 modelSize = item.getMaximum() - item.getMinimum()
808 glNewList(item.modelDisplayList, GL_COMPILE)
809 opengl.DrawMesh(item.mesh)
812 if item.validPlacement:
813 if self.parent.selection == item:
814 glLightfv(GL_LIGHT0, GL_DIFFUSE, map(lambda x: x + 0.2, self.objColor))
815 glLightfv(GL_LIGHT0, GL_AMBIENT, map(lambda x: x / 2, self.objColor))
817 glLightfv(GL_LIGHT0, GL_DIFFUSE, self.objColor)
818 glLightfv(GL_LIGHT0, GL_AMBIENT, map(lambda x: x / 2, self.objColor))
820 if self.parent.selection == item:
821 glLightfv(GL_LIGHT0, GL_DIFFUSE, [1.0, 0.0, 0.0, 0.0])
822 glLightfv(GL_LIGHT0, GL_AMBIENT, [0.2, 0.0, 0.0, 0.0])
824 glLightfv(GL_LIGHT0, GL_DIFFUSE, [1.0, 0.0, 0.0, 0.0])
825 glLightfv(GL_LIGHT0, GL_AMBIENT, [0.2, 0.0, 0.0, 0.0])
828 glEnable(GL_LIGHTING)
829 glTranslate(item.centerX, item.centerY, 0)
831 glScalef(item.scale, item.scale, item.scale)
832 glCallList(item.modelDisplayList)
835 vMin = item.getMinimum() * item.scale
836 vMax = item.getMaximum() * item.scale
837 vMinHead = vMin - extraSizeMin - self.parent.extruderOffset[item.extruder]
838 vMaxHead = vMax + extraSizeMax - self.parent.extruderOffset[item.extruder]
840 glDisable(GL_LIGHTING)
842 if not self.parent.alwaysAutoPlace:
843 if self.parent.selection == item:
845 glColor3f(1.0,0.0,0.3)
847 glColor3f(1.0,0.0,1.0)
848 opengl.DrawBox(vMin, vMax)
850 glColor3f(1.0,0.3,0.0)
852 glColor3f(1.0,1.0,0.0)
853 opengl.DrawBox(vMinHead, vMaxHead)
856 glColor3f(0.5,0.0,0.1)
858 glColor3f(0.5,0.0,0.5)
859 opengl.DrawBox(vMinHead, vMaxHead)
862 glColor3f(0.7,0.1,0.0)
864 glColor3f(0.7,0.7,0.0)
865 opengl.DrawBox(vMin, vMax)
869 opengl.DrawMachine(util3d.Vector3(machineSize[0], machineSize[1], machineSize[2]))
872 class ProjectSliceProgressWindow(wx.Frame):
873 def __init__(self, sliceCommand, resultFilename, fileCount):
874 super(ProjectSliceProgressWindow, self).__init__(None, title='Cura')
875 self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
877 self.sliceCommand = sliceCommand
878 self.resultFilename = resultFilename
879 self.fileCount = fileCount
881 self.prevStep = 'start'
882 self.totalDoneFactor = 0.0
883 self.startTime = time.time()
884 self.sliceStartTime = time.time()
886 self.sizer = wx.GridBagSizer(2, 2)
887 self.statusText = wx.StaticText(self, -1, "Building: %s" % (resultFilename))
888 self.progressGauge = wx.Gauge(self, -1)
889 self.progressGauge.SetRange(10000)
890 self.progressGauge2 = wx.Gauge(self, -1)
891 self.progressGauge2.SetRange(self.fileCount)
892 self.progressGauge2.SetValue(-1)
893 self.abortButton = wx.Button(self, -1, "Abort")
894 self.sizer.Add(self.statusText, (0,0), span=(1,5))
895 self.sizer.Add(self.progressGauge, (1, 0), span=(1,5), flag=wx.EXPAND)
896 self.sizer.Add(self.progressGauge2, (2, 0), span=(1,5), flag=wx.EXPAND)
898 self.sizer.Add(self.abortButton, (3,0), span=(1,5), flag=wx.ALIGN_CENTER)
899 self.sizer.AddGrowableCol(0)
900 self.sizer.AddGrowableRow(0)
902 self.Bind(wx.EVT_BUTTON, self.OnAbort, self.abortButton)
903 self.SetSizer(self.sizer)
907 threading.Thread(target=self.OnRun).start()
909 def OnAbort(self, e):
914 self.abortButton.SetLabel('Close')
916 def SetProgress(self, stepName, layer, maxLayer):
917 if self.prevStep != stepName:
918 if stepName == 'slice':
919 self.progressGauge2.SetValue(self.progressGauge2.GetValue() + 1)
920 self.totalDoneFactor = 0
921 self.totalDoneFactor += sliceRun.sliceStepTimeFactor[self.prevStep]
922 newTime = time.time()
923 #print "#####" + str(newTime-self.startTime) + " " + self.prevStep + " -> " + stepName
924 self.startTime = newTime
925 self.prevStep = stepName
927 progresValue = ((self.totalDoneFactor + sliceRun.sliceStepTimeFactor[stepName] * layer / maxLayer) / sliceRun.totalRunTimeFactor) * 10000
928 self.progressGauge.SetValue(int(progresValue))
929 self.statusText.SetLabel(stepName + " [" + str(layer) + "/" + str(maxLayer) + "]")
930 taskbar.setProgress(self, 10000 * self.progressGauge2.GetValue() + int(progresValue), 10000 * self.fileCount)
933 self.progressLog = []
934 p = sliceRun.startSliceCommandProcess(self.sliceCommand)
935 line = p.stdout.readline()
936 while(len(line) > 0):
938 if line[0:9] == "Progress[" and line[-1:] == "]":
939 progress = line[9:-1].split(":")
940 if len(progress) > 2:
941 maxValue = int(progress[2])
942 wx.CallAfter(self.SetProgress, progress[0], int(progress[1]), maxValue)
944 self.progressLog.append(line)
945 wx.CallAfter(self.statusText.SetLabel, line)
948 wx.CallAfter(self.statusText.SetLabel, "Aborted by user.")
950 line = p.stdout.readline()
951 line = p.stderr.readline()
954 self.progressLog.append(line)
955 line = p.stderr.readline()
956 self.returnCode = p.wait()
957 self.progressGauge2.SetValue(self.fileCount)
959 gcode = gcodeInterpreter.gcode()
960 gcode.load(self.resultFilename)
963 sliceTime = time.time() - self.sliceStartTime
964 status = "Build: %s" % (self.resultFilename)
965 status += "\nSlicing took: %02d:%02d" % (sliceTime / 60, sliceTime % 60)
966 status += "\nFilament: %.2fm %.2fg" % (gcode.extrusionAmount / 1000, gcode.calculateWeight() * 1000)
967 status += "\nPrint time: %02d:%02d" % (int(gcode.totalMoveTimeMinute / 60), int(gcode.totalMoveTimeMinute % 60))
968 cost = gcode.calculateCost()
970 status += "\nCost: %s" % (cost)
971 profile.replaceGCodeTags(self.resultFilename, gcode)
972 wx.CallAfter(self.statusText.SetLabel, status)
973 wx.CallAfter(self.OnSliceDone)
975 def _adjustNumberInLine(self, line, tag, f):
976 m = re.search('^(.*'+tag+')([0-9\.]*)(.*)$', line)
977 return m.group(1) + str(float(m.group(2)) + f) + m.group(3) + '\n'
979 def OnSliceDone(self):
980 self.abortButton.Destroy()
981 self.closeButton = wx.Button(self, -1, "Close")
982 self.printButton = wx.Button(self, -1, "Print")
983 self.logButton = wx.Button(self, -1, "Show log")
984 self.sizer.Add(self.closeButton, (3,0), span=(1,1))
985 self.sizer.Add(self.printButton, (3,1), span=(1,1))
986 self.sizer.Add(self.logButton, (3,2), span=(1,1))
987 if exporer.hasExporer():
988 self.openFileLocationButton = wx.Button(self, -1, "Open file location")
989 self.Bind(wx.EVT_BUTTON, self.OnOpenFileLocation, self.openFileLocationButton)
990 self.sizer.Add(self.openFileLocationButton, (3,3), span=(1,1))
991 if profile.getPreference('sdpath') != '':
992 self.copyToSDButton = wx.Button(self, -1, "To SDCard")
993 self.Bind(wx.EVT_BUTTON, self.OnCopyToSD, self.copyToSDButton)
994 self.sizer.Add(self.copyToSDButton, (3,4), span=(1,1))
995 self.Bind(wx.EVT_BUTTON, self.OnAbort, self.closeButton)
996 self.Bind(wx.EVT_BUTTON, self.OnPrint, self.printButton)
997 self.Bind(wx.EVT_BUTTON, self.OnShowLog, self.logButton)
1000 taskbar.setBusy(self, False)
1002 def OnCopyToSD(self, e):
1003 filename = os.path.basename(self.resultFilename)
1004 if profile.getPreference('sdshortnames') == 'True':
1005 filename = sliceRun.getShortFilename(filename)
1006 shutil.copy(self.resultFilename, os.path.join(profile.getPreference('sdpath'), filename))
1008 def OnOpenFileLocation(self, e):
1009 exporer.openExporer(self.resultFilename)
1011 def OnPrint(self, e):
1012 printWindow.printFile(self.resultFilename)
1014 def OnShowLog(self, e):
1015 LogWindow('\n'.join(self.progressLog))
1017 class preferencesDialog(wx.Frame):
1018 def __init__(self, parent):
1019 super(preferencesDialog, self).__init__(None, title="Project Planner Preferences", style=wx.DEFAULT_DIALOG_STYLE)
1021 self.parent = parent
1022 wx.EVT_CLOSE(self, self.OnClose)
1024 self.panel = configBase.configPanelBase(self)
1025 extruderAmount = int(profile.getPreference('extruder_amount'))
1027 left, right, main = self.panel.CreateConfigPanel(self)
1028 configBase.TitleRow(left, 'User interface settings')
1029 c = configBase.SettingRow(left, 'Always auto place objects in planner', 'planner_always_autoplace', True, 'Disable this to allow manual placement in the project planner (requires restart).', type = 'preference')
1030 configBase.TitleRow(left, 'Machine head size')
1031 c = configBase.SettingRow(left, 'Head size - X towards home (mm)', 'extruder_head_size_min_x', '0', 'Size of your printer head in the X direction, on the Ultimaker your fan is in this direction.', type = 'preference')
1032 validators.validFloat(c, 0.1)
1033 c = configBase.SettingRow(left, 'Head size - X towards end (mm)', 'extruder_head_size_max_x', '0', 'Size of your printer head in the X direction.', type = 'preference')
1034 validators.validFloat(c, 0.1)
1035 c = configBase.SettingRow(left, 'Head size - Y towards home (mm)', 'extruder_head_size_min_y', '0', 'Size of your printer head in the Y direction.', type = 'preference')
1036 validators.validFloat(c, 0.1)
1037 c = configBase.SettingRow(left, 'Head size - Y towards end (mm)', 'extruder_head_size_max_y', '0', 'Size of your printer head in the Y direction.', type = 'preference')
1038 validators.validFloat(c, 0.1)
1039 c = configBase.SettingRow(left, 'Head gantry height (mm)', 'extruder_head_size_height', '0', 'The tallest object height that will always fit under your printers gantry system when the printer head is at the lowest Z position.', type = 'preference')
1040 validators.validFloat(c)
1042 self.okButton = wx.Button(left, -1, 'Ok')
1043 left.GetSizer().Add(self.okButton, (left.GetSizer().GetRows(), 1))
1044 self.okButton.Bind(wx.EVT_BUTTON, self.OnClose)
1046 self.MakeModal(True)
1050 def OnClose(self, e):
1051 self.parent.headSizeMin = numpy.array([profile.getPreferenceFloat('extruder_head_size_min_x'), profile.getPreferenceFloat('extruder_head_size_min_y'),0])
1052 self.parent.headSizeMax = numpy.array([profile.getPreferenceFloat('extruder_head_size_max_x'), profile.getPreferenceFloat('extruder_head_size_max_y'),0])
1053 self.parent.Refresh()
1055 self.MakeModal(False)
1058 class LogWindow(wx.Frame):
1059 def __init__(self, logText):
1060 super(LogWindow, self).__init__(None, title="Slice log")
1061 self.textBox = wx.TextCtrl(self, -1, logText, style=wx.TE_MULTILINE|wx.TE_DONTWRAP|wx.TE_READONLY)
1062 self.SetSize((400,300))