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 if not self._ym.publishDesign(id):
160 #If publishing failed try again after 1 second, this might help when you need to wait for the renderer. But does not always work.
162 self._ym.publishDesign(id)
163 wx.CallAfter(self._indicatorWindow.Hide)
165 webbrowser.open(self._ym.viewUrlForDesign(id))
168 class workingIndicatorWindow(wx.Frame):
169 def __init__(self, parent):
170 super(workingIndicatorWindow, self).__init__(parent, title='YouMagine', style=wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.FRAME_NO_TASKBAR|wx.CAPTION)
171 self._panel = wx.Panel(self)
172 self.SetSizer(wx.BoxSizer())
173 self.GetSizer().Add(self._panel, 1, wx.EXPAND)
175 self._busyBitmaps = [
176 wx.Bitmap(getPathForImage('busy-0.png')),
177 wx.Bitmap(getPathForImage('busy-1.png')),
178 wx.Bitmap(getPathForImage('busy-2.png')),
179 wx.Bitmap(getPathForImage('busy-3.png'))
182 self._indicatorBitmap = wx.StaticBitmap(self._panel, -1, wx.EmptyBitmapRGBA(24, 24, red=255, green=255, blue=255, alpha=1))
183 self._statusText = wx.StaticText(self._panel, -1, '...')
184 self._progress = wx.Gauge(self._panel, -1)
185 self._progress.SetRange(1000)
186 self._progress.SetMinSize((250, 30))
188 self._panel._sizer = wx.GridBagSizer(2, 2)
189 self._panel.SetSizer(self._panel._sizer)
190 self._panel._sizer.Add(self._indicatorBitmap, (0, 0), flag=wx.ALL, border=5)
191 self._panel._sizer.Add(self._statusText, (0, 1), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border=5)
192 self._panel._sizer.Add(self._progress, (1, 0), span=(1,2), flag=wx.EXPAND|wx.ALL, border=5)
195 self._busyTimer = wx.Timer(self)
196 self.Bind(wx.EVT_TIMER, self._busyUpdate, self._busyTimer)
197 self._busyTimer.Start(100)
199 def _busyUpdate(self, e):
200 if self._busyState is None:
203 if self._busyState >= len(self._busyBitmaps):
205 self._indicatorBitmap.SetBitmap(self._busyBitmaps[self._busyState])
207 def progress(self, progressAmount):
208 wx.CallAfter(self._progress.Show)
209 wx.CallAfter(self._progress.SetValue, progressAmount*1000)
210 wx.CallAfter(self.Layout)
211 wx.CallAfter(self.Fit)
213 def showBusy(self, text):
214 self._statusText.SetLabel(text)
215 self._progress.Hide()
221 class getAuthorizationWindow(wx.Frame):
222 def __init__(self, parent, ym):
223 super(getAuthorizationWindow, self).__init__(parent, title='YouMagine')
224 self._panel = wx.Panel(self)
225 self.SetSizer(wx.BoxSizer())
226 self.GetSizer().Add(self._panel, 1, wx.EXPAND)
230 self._requestButton = wx.Button(self._panel, -1, 'Request authorization from YouMagine')
231 self._authToken = wx.TextCtrl(self._panel, -1, 'Paste token here')
233 self._panel._sizer = wx.GridBagSizer(5, 5)
234 self._panel.SetSizer(self._panel._sizer)
236 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)
237 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))
238 self._panel._sizer.Add(self._requestButton, (2, 1), flag=wx.ALL)
239 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)
240 self._panel._sizer.Add(wx.StaticLine(self._panel, -1), (4,0), span=(1,4), flag=wx.EXPAND | wx.ALL)
241 self._panel._sizer.Add(self._authToken, (5, 1), flag=wx.EXPAND | wx.ALL)
242 self._panel._sizer.Add(wx.StaticLine(self._panel, -1), (6,0), span=(1,4), flag=wx.EXPAND | wx.ALL)
244 self.Bind(wx.EVT_BUTTON, self.OnRequestAuthorization, self._requestButton)
245 self.Bind(wx.EVT_TEXT, self.OnEnterToken, self._authToken)
246 self.Bind(wx.EVT_CLOSE, self.OnClose)
251 self._authToken.SetFocus()
252 self._authToken.SelectAll()
254 def OnRequestAuthorization(self, e):
255 webbrowser.open(self._ym.getAuthorizationUrl())
257 def OnEnterToken(self, e):
258 self._ym.setAuthToken(self._authToken.GetValue())
260 def OnClose(self, e):
263 class newDesignWindow(wx.Frame):
264 def __init__(self, parent, manager, ym):
265 super(newDesignWindow, self).__init__(parent, title='Share on YouMagine')
267 self.SetSizer(wx.BoxSizer())
268 self.GetSizer().Add(p, 1, wx.EXPAND)
269 self._manager = manager
272 categoryOptions = ym.getCategories()
273 licenseOptions = ym.getLicenses()
274 self._designName = wx.TextCtrl(p, -1, 'Design name')
275 self._designDescription = wx.TextCtrl(p, -1, '', size=(1, 150), style = wx.TE_MULTILINE)
276 self._designLicense = wx.ComboBox(p, -1, licenseOptions[0], choices=licenseOptions, style=wx.CB_DROPDOWN|wx.CB_READONLY)
277 self._category = wx.ComboBox(p, -1, categoryOptions[-1], choices=categoryOptions, style=wx.CB_DROPDOWN|wx.CB_READONLY)
278 self._publish = wx.CheckBox(p, -1, 'Publish after upload')
279 self._shareButton = wx.Button(p, -1, 'Share!')
280 self._imageScroll = wx.lib.scrolledpanel.ScrolledPanel(p)
281 self._additionalFiles = wx.CheckListBox(p, -1)
282 self._additionalFiles.InsertItems(getAdditionalFiles(self._manager._scene.objects(), True), 0)
283 self._additionalFiles.SetChecked(range(0, self._additionalFiles.GetCount()))
284 self._additionalFiles.InsertItems(getAdditionalFiles(self._manager._scene.objects(), False), self._additionalFiles.GetCount())
286 self._imageScroll.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
287 self._addImageButton = wx.Button(self._imageScroll, -1, 'Add...', size=(70,52))
288 self._imageScroll.GetSizer().Add(self._addImageButton)
289 self._snapshotButton = wx.Button(self._imageScroll, -1, 'Webcam...', size=(70,52))
290 self._imageScroll.GetSizer().Add(self._snapshotButton)
291 self._imageScroll.Fit()
292 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
293 self._imageScroll.SetMinSize((20, self._imageScroll.GetSize()[1] + wx.SystemSettings_GetMetric(wx.SYS_HSCROLL_Y)))
295 self._publish.SetValue(True)
296 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')
298 s = wx.GridBagSizer(5, 5)
301 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)
302 s.Add(wx.StaticText(p, -1, 'Design name:'), (1, 0), flag=wx.LEFT|wx.TOP, border=5)
303 s.Add(self._designName, (1, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
304 s.Add(wx.StaticText(p, -1, 'Description:'), (2, 0), flag=wx.LEFT|wx.TOP, border=5)
305 s.Add(self._designDescription, (2, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
306 s.Add(wx.StaticText(p, -1, 'Category:'), (3, 0), flag=wx.LEFT|wx.TOP, border=5)
307 s.Add(self._category, (3, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
308 s.Add(wx.StaticText(p, -1, 'License:'), (4, 0), flag=wx.LEFT|wx.TOP, border=5)
309 s.Add(self._designLicense, (4, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
310 s.Add(wx.StaticLine(p, -1), (5,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
311 s.Add(wx.StaticText(p, -1, 'Images:'), (6, 0), flag=wx.LEFT|wx.TOP, border=5)
312 s.Add(self._imageScroll, (6, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
313 s.Add(wx.StaticLine(p, -1), (7,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
314 s.Add(wx.StaticText(p, -1, 'Related design files:'), (8, 0), flag=wx.LEFT|wx.TOP, border=5)
315 s.Add(self._additionalFiles, (8, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
316 s.Add(wx.StaticLine(p, -1), (9,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
317 s.Add(self._shareButton, (10, 1), flag=wx.BOTTOM, border=15)
318 s.Add(self._publish, (10, 2), flag=wx.BOTTOM|wx.ALIGN_CENTER_VERTICAL, border=15)
323 self.Bind(wx.EVT_BUTTON, self.OnShare, self._shareButton)
324 self.Bind(wx.EVT_BUTTON, self.OnAddImage, self._addImageButton)
325 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._snapshotButton)
330 self._designDescription.SetMinSize((1,1))
331 self._designName.SetFocus()
332 self._designName.SelectAll()
334 def OnShare(self, e):
335 if self._designName.GetValue() == '':
336 wx.MessageBox('The name cannot be empty', 'New design error.', wx.OK | wx.ICON_ERROR)
337 self._designName.SetFocus()
339 if self._designDescription.GetValue() == '':
340 wx.MessageBox('The description cannot be empty', 'New design error.', wx.OK | wx.ICON_ERROR)
341 self._designDescription.SetFocus()
344 for child in self._imageScroll.GetChildren():
345 if hasattr(child, 'imageFilename'):
346 imageList.append(child.imageFilename)
347 if hasattr(child, 'imageData'):
348 imageList.append(child.imageData)
349 self._manager.createNewDesign(self._designName.GetValue(), self._designDescription.GetValue(), self._category.GetValue(), self._designLicense.GetValue(), imageList, self._additionalFiles.GetCheckedStrings(), self._publish.GetValue())
352 def OnAddImage(self, e):
353 dlg=wx.FileDialog(self, "Select image file...", style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE)
354 dlg.SetWildcard("Image files (*.jpg,*.jpeg,*.png)|*.jpg;*.jpeg;*.png")
355 if dlg.ShowModal() == wx.ID_OK:
356 for filename in dlg.GetPaths():
357 self._addImage(filename)
360 def OnTakeImage(self, e):
361 webcamPhotoWindow(self).Show()
363 def _addImage(self, image):
365 if type(image) in types.StringTypes:
367 wxImage = wx.ImageFromBitmap(wx.Bitmap(image))
371 wxImage = wx.ImageFromBitmap(image)
375 width, height = wxImage.GetSize()
377 height = height*70/width
380 width = width*52/height
382 wxImage.Rescale(width, height, wx.IMAGE_QUALITY_NORMAL)
383 wxImage.Resize((70, 52), ((70-width)/2, (52-height)/2))
384 ctrl = wx.StaticBitmap(self._imageScroll, -1, wx.BitmapFromImage(wxImage))
385 if type(image) in types.StringTypes:
386 ctrl.imageFilename = image
388 ctrl.imageData = image
390 delButton = wx.Button(ctrl, -1, 'X', style=wx.BU_EXACTFIT)
391 self.Bind(wx.EVT_BUTTON, self.OnDeleteImage, delButton)
393 self._imageScroll.GetSizer().Insert(len(self._imageScroll.GetChildren())-3, ctrl)
394 self._imageScroll.Layout()
395 self._imageScroll.Refresh()
396 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
398 def OnDeleteImage(self, e):
399 ctrl = e.GetEventObject().GetParent()
400 self._imageScroll.GetSizer().Detach(ctrl)
403 self._imageScroll.Layout()
404 self._imageScroll.Refresh()
405 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
407 class webcamPhotoWindow(wx.Frame):
408 def __init__(self, parent):
409 super(webcamPhotoWindow, self).__init__(parent, title='YouMagine')
412 self.SetSizer(wx.BoxSizer())
413 self.GetSizer().Add(p, 1, wx.EXPAND)
415 self._cam = webcam.webcam()
416 self._cam.takeNewImage(False)
418 s = wx.GridBagSizer(3, 3)
421 self._preview = wx.Panel(p)
422 self._cameraSelect = wx.ComboBox(p, -1, self._cam.listCameras()[0], choices=self._cam.listCameras(), style=wx.CB_DROPDOWN|wx.CB_READONLY)
423 self._takeImageButton = wx.Button(p, -1, 'Snap image')
424 self._takeImageTimer = wx.Timer(self)
426 s.Add(self._takeImageButton, pos=(1, 0), flag=wx.ALL, border=5)
427 s.Add(self._cameraSelect, pos=(1, 1), flag=wx.ALL, border=5)
428 s.Add(self._preview, pos=(0, 0), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=5)
430 if self._cam.getLastImage() is not None:
431 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
433 self._preview.SetMinSize((640, 480))
435 self._preview.Bind(wx.EVT_ERASE_BACKGROUND, self.OnCameraEraseBackground)
436 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._takeImageButton)
437 self.Bind(wx.EVT_TIMER, self.OnTakeImageTimer, self._takeImageTimer)
438 self.Bind(wx.EVT_COMBOBOX, self.OnCameraChange, self._cameraSelect)
443 self._takeImageTimer.Start(200)
445 def OnCameraChange(self, e):
446 self._cam.setActiveCamera(self._cameraSelect.GetSelection())
448 def OnTakeImage(self, e):
449 self.GetParent()._addImage(self._cam.getLastImage())
452 def OnTakeImageTimer(self, e):
453 self._cam.takeNewImage(False)
456 def OnCameraEraseBackground(self, e):
459 dc = wx.ClientDC(self)
460 rect = self.GetUpdateRegion().GetBox()
461 dc.SetClippingRect(rect)
462 dc.SetBackground(wx.Brush(self._preview.GetBackgroundColour(), wx.SOLID))
463 if self._cam.getLastImage() is not None:
464 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
466 dc.DrawBitmap(self._cam.getLastImage(), 0, 0)