1 from __future__ import absolute_import
2 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
11 import cStringIO as StringIO
13 from Cura.util import profile
14 from Cura.util import youmagine
15 from Cura.util.meshLoaders import stl
16 from Cura.util.meshLoaders import amf
17 from Cura.util.resources import getPathForImage
19 from Cura.gui.util import webcam
21 DESIGN_FILE_EXT = ['.scad', '.blend', '.max', '.stp', '.step', '.igs', '.iges', '.sldasm', '.sldprt', '.skp', '.ipt', '.dwg', '.123d', '.wings']
23 def getClipboardText():
26 if not wx.TheClipboard.IsOpened():
27 wx.TheClipboard.Open()
28 do = wx.TextDataObject()
29 if wx.TheClipboard.GetData(do):
31 wx.TheClipboard.Close()
36 def getAdditionalFiles(objects, onlyExtChanged):
39 name = obj.getOriginFilename()
42 names.add(name[:name.rfind('.')])
45 for ext in DESIGN_FILE_EXT:
46 if os.path.isfile(name + ext):
47 ret.append(name + ext)
53 for ext in DESIGN_FILE_EXT:
54 for filename in os.listdir(os.path.dirname(name)):
55 filename = os.path.join(os.path.dirname(name), filename)
56 if filename.endswith(ext) and filename not in ret and filename not in onlyExtList:
60 class youmagineManager(object):
61 def __init__(self, parent, objectScene):
62 self._mainWindow = parent
63 self._scene = objectScene
64 self._ym = youmagine.Youmagine(profile.getPreference('youmagine_token'), self._progressCallback)
66 self._indicatorWindow = workingIndicatorWindow(self._mainWindow)
67 self._getAuthorizationWindow = getAuthorizationWindow(self._mainWindow, self._ym)
68 self._newDesignWindow = newDesignWindow(self._mainWindow, self, self._ym)
70 thread = threading.Thread(target=self.checkAuthorizationThread)
74 def _progressCallback(self, progress):
75 self._indicatorWindow.progress(progress)
77 #Do all the youmagine communication in a background thread, because it can take a while and block the UI thread otherwise
78 def checkAuthorizationThread(self):
79 wx.CallAfter(self._indicatorWindow.showBusy, 'Checking token')
80 if not self._ym.isAuthorized():
81 wx.CallAfter(self._indicatorWindow.Hide)
82 if not self._ym.isHostReachable():
83 wx.MessageBox('Failed to contact YouMagine.com', 'YouMagine error.', wx.OK | wx.ICON_ERROR)
85 wx.CallAfter(self._getAuthorizationWindow.Show)
86 lastTriedClipboard = ''
87 while not self._ym.isAuthorized():
89 if self._getAuthorizationWindow.abort:
90 wx.CallAfter(self._getAuthorizationWindow.Destroy)
92 clipboard = getClipboardText()
93 if len(clipboard) == 20:
94 if clipboard != lastTriedClipboard and re.match('[a-zA-Z0-9]*', clipboard):
95 lastTriedClipboard = clipboard
96 self._ym.setAuthToken(clipboard)
97 profile.putPreference('youmagine_token', self._ym.getAuthToken())
98 wx.CallAfter(self._getAuthorizationWindow.Hide)
99 wx.CallAfter(self._getAuthorizationWindow.Destroy)
100 wx.MessageBox('Cura is now authorized to share on YouMagine', 'YouMagine.', wx.OK | wx.ICON_INFORMATION)
101 wx.CallAfter(self._indicatorWindow.Hide)
103 #TODO: Would you like to create a new design or add the model to an existing design?
104 wx.CallAfter(self._newDesignWindow.Show)
106 def createNewDesign(self, name, description, category, license, imageList, extraFileList, publish):
107 thread = threading.Thread(target=self.createNewDesignThread, args=(name, description, category, license, imageList, extraFileList, publish))
111 def createNewDesignThread(self, name, description, category, license, imageList, extraFileList, publish):
112 wx.CallAfter(self._indicatorWindow.showBusy, 'Creating new design on YouMagine...')
113 id = self._ym.createDesign(name, description, category, license)
114 wx.CallAfter(self._indicatorWindow.Hide)
116 wx.MessageBox('Failed to create a design, nothing uploaded!', 'YouMagine error.', wx.OK | wx.ICON_ERROR)
119 for obj in self._scene.objects():
120 wx.CallAfter(self._indicatorWindow.showBusy, 'Building model %s...' % (obj.getName()))
122 s = StringIO.StringIO()
123 filename = obj.getName()
124 if obj.canStoreAsSTL():
125 stl.saveSceneStream(s, [obj])
128 amf.saveSceneStream(s, filename, [obj])
131 wx.CallAfter(self._indicatorWindow.showBusy, 'Uploading model %s...' % (filename))
132 if self._ym.createDocument(id, filename, s.getvalue()) is None:
133 wx.MessageBox('Failed to upload %s!' % (filename), 'YouMagine error.', wx.OK | wx.ICON_ERROR)
136 for extra in extraFileList:
137 wx.CallAfter(self._indicatorWindow.showBusy, 'Uploading file %s...' % (os.path.basename(extra)))
138 with open(extra, "rb") as f:
139 if self._ym.createDocument(id, os.path.basename(extra), f.read()) is None:
140 wx.MessageBox('Failed to upload %s!' % (os.path.basename(extra)), 'YouMagine error.', wx.OK | wx.ICON_ERROR)
142 for image in imageList:
143 if type(image) in types.StringTypes:
144 filename = os.path.basename(image)
145 wx.CallAfter(self._indicatorWindow.showBusy, 'Uploading image %s...' % (filename))
146 with open(image, "rb") as f:
147 if self._ym.createImage(id, filename, f.read()) is None:
148 wx.MessageBox('Failed to upload %s!' % (filename), 'YouMagine error.', wx.OK | wx.ICON_ERROR)
149 elif type(image) is wx.Bitmap:
150 s = StringIO.StringIO()
151 if wx.ImageFromBitmap(image).SaveStream(s, wx.BITMAP_TYPE_JPEG):
152 if self._ym.createImage(id, "snapshot.jpg", s.getvalue()) is None:
153 wx.MessageBox('Failed to upload snapshot!', 'YouMagine error.', wx.OK | wx.ICON_ERROR)
158 wx.CallAfter(self._indicatorWindow.showBusy, 'Publishing design...')
159 self._ym.publishDesign(id)
160 wx.CallAfter(self._indicatorWindow.Hide)
162 webbrowser.open(self._ym.viewUrlForDesign(id))
165 class workingIndicatorWindow(wx.Frame):
166 def __init__(self, parent):
167 super(workingIndicatorWindow, self).__init__(parent, title='YouMagine', style=wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.FRAME_NO_TASKBAR|wx.CAPTION)
168 self._panel = wx.Panel(self)
169 self.SetSizer(wx.BoxSizer())
170 self.GetSizer().Add(self._panel, 1, wx.EXPAND)
172 self._busyBitmaps = [
173 wx.Bitmap(getPathForImage('busy-0.png')),
174 wx.Bitmap(getPathForImage('busy-1.png')),
175 wx.Bitmap(getPathForImage('busy-2.png')),
176 wx.Bitmap(getPathForImage('busy-3.png'))
179 self._indicatorBitmap = wx.StaticBitmap(self._panel, -1, wx.EmptyBitmapRGBA(24, 24, red=255, green=255, blue=255, alpha=1))
180 self._statusText = wx.StaticText(self._panel, -1, '...')
181 self._progress = wx.Gauge(self._panel, -1)
182 self._progress.SetRange(1000)
183 self._progress.SetMinSize((250, 30))
185 self._panel._sizer = wx.GridBagSizer(2, 2)
186 self._panel.SetSizer(self._panel._sizer)
187 self._panel._sizer.Add(self._indicatorBitmap, (0, 0), flag=wx.ALL, border=5)
188 self._panel._sizer.Add(self._statusText, (0, 1), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border=5)
189 self._panel._sizer.Add(self._progress, (1, 0), span=(1,2), flag=wx.EXPAND|wx.ALL, border=5)
192 self._busyTimer = wx.Timer(self)
193 self.Bind(wx.EVT_TIMER, self._busyUpdate, self._busyTimer)
194 self._busyTimer.Start(100)
196 def _busyUpdate(self, e):
197 if self._busyState is None:
200 if self._busyState >= len(self._busyBitmaps):
202 self._indicatorBitmap.SetBitmap(self._busyBitmaps[self._busyState])
204 def progress(self, progressAmount):
205 wx.CallAfter(self._progress.Show)
206 wx.CallAfter(self._progress.SetValue, progressAmount*1000)
207 wx.CallAfter(self.Layout)
208 wx.CallAfter(self.Fit)
210 def showBusy(self, text):
211 self._statusText.SetLabel(text)
212 self._progress.Hide()
218 class getAuthorizationWindow(wx.Frame):
219 def __init__(self, parent, ym):
220 super(getAuthorizationWindow, self).__init__(parent, title='YouMagine')
221 self._panel = wx.Panel(self)
222 self.SetSizer(wx.BoxSizer())
223 self.GetSizer().Add(self._panel, 1, wx.EXPAND)
227 self._requestButton = wx.Button(self._panel, -1, 'Request authorization from YouMagine')
228 self._authToken = wx.TextCtrl(self._panel, -1, 'Paste token here')
230 self._panel._sizer = wx.GridBagSizer(5, 5)
231 self._panel.SetSizer(self._panel._sizer)
233 self._panel._sizer.Add(wx.StaticBitmap(self._panel, -1, wx.Bitmap(getPathForImage('youmagine-text.png'))), (0,0), span=(1,4), flag=wx.ALIGN_CENTRE | wx.ALL, border=5)
234 self._panel._sizer.Add(wx.StaticText(self._panel, -1, 'To share your designs on YouMagine\nyou need an account on YouMagine.com\nand authorize Cura to access your account.'), (1, 1))
235 self._panel._sizer.Add(self._requestButton, (2, 1), flag=wx.ALL)
236 self._panel._sizer.Add(wx.StaticText(self._panel, -1, 'This will open a browser window where you can\nauthorize Cura to access your YouMagine account.\nYou can revoke access at any time\nfrom YouMagine.com'), (3, 1), flag=wx.ALL)
237 self._panel._sizer.Add(wx.StaticLine(self._panel, -1), (4,0), span=(1,4), flag=wx.EXPAND | wx.ALL)
238 self._panel._sizer.Add(self._authToken, (5, 1), flag=wx.EXPAND | wx.ALL)
239 self._panel._sizer.Add(wx.StaticLine(self._panel, -1), (6,0), span=(1,4), flag=wx.EXPAND | wx.ALL)
241 self.Bind(wx.EVT_BUTTON, self.OnRequestAuthorization, self._requestButton)
242 self.Bind(wx.EVT_TEXT, self.OnEnterToken, self._authToken)
243 self.Bind(wx.EVT_CLOSE, self.OnClose)
248 self._authToken.SetFocus()
249 self._authToken.SelectAll()
251 def OnRequestAuthorization(self, e):
252 webbrowser.open(self._ym.getAuthorizationUrl())
254 def OnEnterToken(self, e):
255 self._ym.setAuthToken(self._authToken.GetValue())
257 def OnClose(self, e):
260 class newDesignWindow(wx.Frame):
261 def __init__(self, parent, manager, ym):
262 super(newDesignWindow, self).__init__(parent, title='YouMagine')
264 self.SetSizer(wx.BoxSizer())
265 self.GetSizer().Add(p, 1, wx.EXPAND)
266 self._manager = manager
269 categoryOptions = ym.getCategories()
270 licenseOptions = ym.getLicenses()
271 self._designName = wx.TextCtrl(p, -1, 'Design name')
272 self._designDescription = wx.TextCtrl(p, -1, '', size=(1, 150), style = wx.TE_MULTILINE)
273 self._designLicense = wx.ComboBox(p, -1, licenseOptions[0], choices=licenseOptions, style=wx.CB_DROPDOWN|wx.CB_READONLY)
274 self._category = wx.ComboBox(p, -1, categoryOptions[-1], choices=categoryOptions, style=wx.CB_DROPDOWN|wx.CB_READONLY)
275 self._publish = wx.CheckBox(p, -1, 'Publish after upload')
276 self._shareButton = wx.Button(p, -1, 'Upload')
277 self._imageScroll = wx.lib.scrolledpanel.ScrolledPanel(p)
278 self._additionalFiles = wx.CheckListBox(p, -1)
279 self._additionalFiles.InsertItems(getAdditionalFiles(self._manager._scene.objects(), True), 0)
280 self._additionalFiles.SetChecked(range(0, self._additionalFiles.GetCount()))
281 self._additionalFiles.InsertItems(getAdditionalFiles(self._manager._scene.objects(), False), self._additionalFiles.GetCount())
283 self._imageScroll.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
284 self._addImageButton = wx.Button(self._imageScroll, -1, 'Add...', size=(70,52))
285 self._imageScroll.GetSizer().Add(self._addImageButton)
286 self._snapshotButton = wx.Button(self._imageScroll, -1, 'Webcam...', size=(70,52))
287 self._imageScroll.GetSizer().Add(self._snapshotButton)
288 self._imageScroll.Fit()
289 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
290 self._imageScroll.SetMinSize((20, self._imageScroll.GetSize()[1] + wx.SystemSettings_GetMetric(wx.SYS_HSCROLL_Y)))
292 self._publish.SetValue(True)
293 self._publish.SetToolTipString('Directly publish the design after uploading.\nWithout this check the design will not be public\nuntil you publish it yourself on YouMagine.com')
295 s = wx.GridBagSizer(5, 5)
298 s.Add(wx.StaticBitmap(p, -1, wx.Bitmap(getPathForImage('youmagine-text.png'))), (0,0), span=(1,3), flag=wx.ALIGN_CENTRE | wx.ALL, border=5)
299 s.Add(wx.StaticText(p, -1, 'Design name:'), (1, 0), flag=wx.LEFT|wx.TOP, border=5)
300 s.Add(self._designName, (1, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
301 s.Add(wx.StaticText(p, -1, 'Description:'), (2, 0), flag=wx.LEFT|wx.TOP, border=5)
302 s.Add(self._designDescription, (2, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
303 s.Add(wx.StaticText(p, -1, 'Category:'), (3, 0), flag=wx.LEFT|wx.TOP, border=5)
304 s.Add(self._category, (3, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
305 s.Add(wx.StaticText(p, -1, 'License:'), (4, 0), flag=wx.LEFT|wx.TOP, border=5)
306 s.Add(self._designLicense, (4, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
307 s.Add(wx.StaticLine(p, -1), (5,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
308 s.Add(wx.StaticText(p, -1, 'Images:'), (6, 0), flag=wx.LEFT|wx.TOP, border=5)
309 s.Add(self._imageScroll, (6, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
310 s.Add(wx.StaticLine(p, -1), (7,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
311 s.Add(wx.StaticText(p, -1, 'Design files:'), (8, 0), flag=wx.LEFT|wx.TOP, border=5)
312 s.Add(self._additionalFiles, (8, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
313 s.Add(wx.StaticLine(p, -1), (9,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
314 s.Add(self._shareButton, (10, 1), flag=wx.BOTTOM, border=15)
315 s.Add(self._publish, (10, 2), flag=wx.BOTTOM|wx.ALIGN_CENTER_VERTICAL, border=15)
320 self.Bind(wx.EVT_BUTTON, self.OnShare, self._shareButton)
321 self.Bind(wx.EVT_BUTTON, self.OnAddImage, self._addImageButton)
322 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._snapshotButton)
327 self._designDescription.SetMinSize((1,1))
328 self._designName.SetFocus()
329 self._designName.SelectAll()
331 def OnShare(self, e):
332 if self._designName.GetValue() == '':
333 wx.MessageBox('The name cannot be empty', 'New design error.', wx.OK | wx.ICON_ERROR)
334 self._designName.SetFocus()
336 if self._designDescription.GetValue() == '':
337 wx.MessageBox('The description cannot be empty', 'New design error.', wx.OK | wx.ICON_ERROR)
338 self._designDescription.SetFocus()
341 for child in self._imageScroll.GetChildren():
342 if hasattr(child, 'imageFilename'):
343 imageList.append(child.imageFilename)
344 if hasattr(child, 'imageData'):
345 imageList.append(child.imageData)
346 self._manager.createNewDesign(self._designName.GetValue(), self._designDescription.GetValue(), self._category.GetValue(), self._designLicense.GetValue(), imageList, self._additionalFiles.GetCheckedStrings(), self._publish.GetValue())
349 def OnAddImage(self, e):
350 dlg=wx.FileDialog(self, "Select image file...", style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE)
351 dlg.SetWildcard("Image files (*.jpg,*.jpeg,*.png)|*.jpg;*.jpeg;*.png")
352 if dlg.ShowModal() == wx.ID_OK:
353 for filename in dlg.GetPaths():
354 self._addImage(filename)
357 def OnTakeImage(self, e):
358 webcamPhotoWindow(self).Show()
360 def _addImage(self, image):
362 if type(image) in types.StringTypes:
364 wxImage = wx.ImageFromBitmap(wx.Bitmap(image))
368 wxImage = wx.ImageFromBitmap(image)
372 width, height = wxImage.GetSize()
374 height = height*70/width
377 width = width*52/height
379 wxImage.Rescale(width, height, wx.IMAGE_QUALITY_NORMAL)
380 wxImage.Resize((70, 52), ((70-width)/2, (52-height)/2))
381 ctrl = wx.StaticBitmap(self._imageScroll, -1, wx.BitmapFromImage(wxImage))
382 if type(image) in types.StringTypes:
383 ctrl.imageFilename = image
385 ctrl.imageData = image
387 delButton = wx.Button(ctrl, -1, 'X', style=wx.BU_EXACTFIT)
388 self.Bind(wx.EVT_BUTTON, self.OnDeleteImage, delButton)
390 self._imageScroll.GetSizer().Insert(len(self._imageScroll.GetChildren())-3, ctrl)
391 self._imageScroll.Layout()
392 self._imageScroll.Refresh()
393 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
395 def OnDeleteImage(self, e):
396 ctrl = e.GetEventObject().GetParent()
397 self._imageScroll.GetSizer().Detach(ctrl)
400 self._imageScroll.Layout()
401 self._imageScroll.Refresh()
402 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
404 class webcamPhotoWindow(wx.Frame):
405 def __init__(self, parent):
406 super(webcamPhotoWindow, self).__init__(parent, title='YouMagine')
409 self.SetSizer(wx.BoxSizer())
410 self.GetSizer().Add(p, 1, wx.EXPAND)
412 self._cam = webcam.webcam()
413 self._cam.takeNewImage(False)
415 s = wx.GridBagSizer(3, 3)
418 self._preview = wx.Panel(p)
419 self._cameraSelect = wx.ComboBox(p, -1, self._cam.listCameras()[0], choices=self._cam.listCameras(), style=wx.CB_DROPDOWN|wx.CB_READONLY)
420 self._takeImageButton = wx.Button(p, -1, 'Snap image')
421 self._takeImageTimer = wx.Timer(self)
423 s.Add(self._takeImageButton, pos=(1, 0), flag=wx.ALL, border=5)
424 s.Add(self._cameraSelect, pos=(1, 1), flag=wx.ALL, border=5)
425 s.Add(self._preview, pos=(0, 0), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=5)
427 if self._cam.getLastImage() is not None:
428 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
430 self._preview.SetMinSize((640, 480))
432 self._preview.Bind(wx.EVT_ERASE_BACKGROUND, self.OnCameraEraseBackground)
433 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._takeImageButton)
434 self.Bind(wx.EVT_TIMER, self.OnTakeImageTimer, self._takeImageTimer)
435 self.Bind(wx.EVT_COMBOBOX, self.OnCameraChange, self._cameraSelect)
440 self._takeImageTimer.Start(200)
442 def OnCameraChange(self, e):
443 self._cam.setActiveCamera(self._cameraSelect.GetSelection())
445 def OnTakeImage(self, e):
446 self.GetParent()._addImage(self._cam.getLastImage())
449 def OnTakeImageTimer(self, e):
450 self._cam.takeNewImage(False)
453 def OnCameraEraseBackground(self, e):
456 dc = wx.ClientDC(self)
457 rect = self.GetUpdateRegion().GetBox()
458 dc.SetClippingRect(rect)
459 dc.SetBackground(wx.Brush(self._preview.GetBackgroundColour(), wx.SOLID))
460 if self._cam.getLastImage() is not None:
461 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
463 dc.DrawBitmap(self._cam.getLastImage(), 0, 0)