CuraApplication.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from UM.Qt.QtApplication import QtApplication
  4. from UM.Scene.SceneNode import SceneNode
  5. from UM.Scene.Camera import Camera
  6. from UM.Scene.Platform import Platform
  7. from UM.Math.Vector import Vector
  8. from UM.Math.Quaternion import Quaternion
  9. from UM.Math.AxisAlignedBox import AxisAlignedBox
  10. from UM.Resources import Resources
  11. from UM.Scene.ToolHandle import ToolHandle
  12. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  13. from UM.Mesh.ReadMeshJob import ReadMeshJob
  14. from UM.Logger import Logger
  15. from UM.Preferences import Preferences
  16. from UM.JobQueue import JobQueue
  17. from UM.SaveFile import SaveFile
  18. from UM.Scene.Selection import Selection
  19. from UM.Scene.GroupDecorator import GroupDecorator
  20. from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
  21. from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
  22. from UM.Operations.GroupedOperation import GroupedOperation
  23. from UM.Operations.SetTransformOperation import SetTransformOperation
  24. from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
  25. from UM.Settings.ContainerRegistry import ContainerRegistry
  26. from UM.i18n import i18nCatalog
  27. from . import PlatformPhysics
  28. from . import BuildVolume
  29. from . import CameraAnimation
  30. from . import PrintInformation
  31. from . import CuraActions
  32. from . import MultiMaterialDecorator
  33. from . import ZOffsetDecorator
  34. from . import CuraSplashScreen
  35. from . import MachineManagerModel
  36. from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
  37. from PyQt5.QtGui import QColor, QIcon
  38. from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType
  39. import platform
  40. import sys
  41. import os.path
  42. import numpy
  43. import copy
  44. import urllib
  45. numpy.seterr(all="ignore")
  46. #WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
  47. if platform.system() == "Linux": # Needed for platform.linux_distribution, which is not available on Windows and OSX
  48. # For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
  49. if platform.linux_distribution()[0] in ("Ubuntu", ): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
  50. import ctypes
  51. from ctypes.util import find_library
  52. ctypes.CDLL(find_library('GL'), ctypes.RTLD_GLOBAL)
  53. try:
  54. from cura.CuraVersion import CuraVersion
  55. except ImportError:
  56. CuraVersion = "master" # [CodeStyle: Reflecting imported value]
  57. class CuraApplication(QtApplication):
  58. class ResourceTypes:
  59. QmlFiles = Resources.UserType + 1
  60. Firmware = Resources.UserType + 2
  61. QualityInstanceContainer = Resources.UserType + 3
  62. MaterialInstanceContainer = Resources.UserType + 4
  63. VariantInstanceContainer = Resources.UserType + 5
  64. UserInstanceContainer = Resources.UserType + 6
  65. MachineStack = Resources.UserType + 7
  66. ExtruderInstanceContainer = Resources.UserType + 8
  67. Q_ENUMS(ResourceTypes)
  68. def __init__(self):
  69. Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources"))
  70. if not hasattr(sys, "frozen"):
  71. Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
  72. self._open_file_queue = [] # Files to open when plug-ins are loaded.
  73. # Need to do this before ContainerRegistry tries to load the machines
  74. SettingDefinition.addSupportedProperty("global_only", DefinitionPropertyType.Function, default = False)
  75. super().__init__(name = "cura", version = CuraVersion)
  76. self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
  77. self.setRequiredPlugins([
  78. "CuraEngineBackend",
  79. "MeshView",
  80. "LayerView",
  81. "STLReader",
  82. "SelectionTool",
  83. "CameraTool",
  84. "GCodeWriter",
  85. "LocalFileOutputDevice"
  86. ])
  87. self._physics = None
  88. self._volume = None
  89. self._platform = None
  90. self._output_devices = {}
  91. self._print_information = None
  92. self._i18n_catalog = None
  93. self._previous_active_tool = None
  94. self._platform_activity = False
  95. self._scene_boundingbox = AxisAlignedBox()
  96. self._job_name = None
  97. self._center_after_select = False
  98. self._camera_animation = None
  99. self._cura_actions = None
  100. self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
  101. self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
  102. Resources.addType(self.ResourceTypes.QmlFiles, "qml")
  103. Resources.addType(self.ResourceTypes.Firmware, "firmware")
  104. ## Add the 4 types of profiles to storage.
  105. Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
  106. Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
  107. Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
  108. Resources.addStorageType(self.ResourceTypes.ExtruderInstanceContainer, "extruders")
  109. Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
  110. Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
  111. ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
  112. ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
  113. ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
  114. ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
  115. ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
  116. ContainerRegistry.getInstance().load()
  117. Preferences.getInstance().addPreference("cura/active_mode", "simple")
  118. Preferences.getInstance().addPreference("cura/recent_files", "")
  119. Preferences.getInstance().addPreference("cura/categories_expanded", "")
  120. Preferences.getInstance().addPreference("view/center_on_select", True)
  121. Preferences.getInstance().addPreference("mesh/scale_to_fit", True)
  122. Preferences.getInstance().setDefault("local_file/last_used_type", "text/x-gcode")
  123. Preferences.getInstance().setDefault("general/visible_settings", """
  124. resolution
  125. layer_height
  126. shell
  127. wall_thickness
  128. top_bottom_thickness
  129. infill
  130. infill_sparse_density
  131. material
  132. material_print_temperature
  133. material_bed_temperature
  134. material_diameter
  135. material_flow
  136. retraction_enable
  137. speed
  138. speed_print
  139. speed_travel
  140. cooling
  141. cool_fan_enabled
  142. support
  143. support_enable
  144. support_type
  145. support_roof_density
  146. platform_adhesion
  147. adhesion_type
  148. brim_width
  149. raft_airgap
  150. layer_0_z_overlap
  151. raft_surface_layers
  152. blackmagic
  153. print_sequence
  154. """)
  155. JobQueue.getInstance().jobFinished.connect(self._onJobFinished)
  156. self.applicationShuttingDown.connect(self._onExit)
  157. self._recent_files = []
  158. files = Preferences.getInstance().getValue("cura/recent_files").split(";")
  159. for f in files:
  160. if not os.path.isfile(f):
  161. continue
  162. self._recent_files.append(QUrl.fromLocalFile(f))
  163. ## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
  164. def _onExit(self):
  165. for instance in ContainerRegistry.getInstance().findInstanceContainers():
  166. if not instance.isDirty():
  167. continue
  168. try:
  169. data = instance.serialize()
  170. except NotImplementedError:
  171. continue
  172. except Exception:
  173. Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
  174. continue
  175. file_name = urllib.parse.quote_plus(instance.getId()) + ".inst.cfg"
  176. instance_type = instance.getMetaDataEntry("type")
  177. path = None
  178. if instance_type == "material":
  179. path = Resources.getStoragePath(self.ResourceTypes.MaterialInstanceContainer, file_name)
  180. elif instance_type == "quality":
  181. path = Resources.getStoragePath(self.ResourceTypes.QualityInstanceContainer, file_name)
  182. elif instance_type == "user":
  183. path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
  184. elif instance_type == "variant":
  185. path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
  186. if path:
  187. with SaveFile(path, "wt", -1, "utf-8") as f:
  188. f.write(data)
  189. for stack in ContainerRegistry.getInstance().findContainerStacks():
  190. if not stack.isDirty():
  191. continue
  192. try:
  193. data = stack.serialize()
  194. except NotImplementedError:
  195. continue
  196. except Exception:
  197. Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
  198. continue
  199. file_name = urllib.parse.quote_plus(stack.getId()) + ".stack.cfg"
  200. path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
  201. with SaveFile(path, "wt", -1, "utf-8") as f:
  202. f.write(data)
  203. @pyqtSlot(result = QUrl)
  204. def getDefaultPath(self):
  205. return QUrl.fromLocalFile(os.path.expanduser("~/"))
  206. ## Handle loading of all plugin types (and the backend explicitly)
  207. # \sa PluginRegistery
  208. def _loadPlugins(self):
  209. self._plugin_registry.addType("profile_reader", self._addProfileReader)
  210. self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura"))
  211. if not hasattr(sys, "frozen"):
  212. self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
  213. self._plugin_registry.loadPlugin("ConsoleLogger")
  214. self._plugin_registry.loadPlugin("CuraEngineBackend")
  215. self._plugin_registry.loadPlugins()
  216. if self.getBackend() == None:
  217. raise RuntimeError("Could not load the backend plugin!")
  218. self._plugins_loaded = True
  219. def addCommandLineOptions(self, parser):
  220. super().addCommandLineOptions(parser)
  221. parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
  222. parser.add_argument("--debug", dest="debug-mode", action="store_true", default=False, help="Enable detailed crash reports.")
  223. def run(self):
  224. self._i18n_catalog = i18nCatalog("cura");
  225. i18nCatalog.setTagReplacements({
  226. "filename": "font color=\"black\"",
  227. "message": "font color=UM.Theme.colors.message_text;",
  228. })
  229. self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
  230. controller = self.getController()
  231. controller.setActiveView("SolidView")
  232. controller.setCameraTool("CameraTool")
  233. controller.setSelectionTool("SelectionTool")
  234. t = controller.getTool("TranslateTool")
  235. if t:
  236. t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis,ToolHandle.ZAxis])
  237. Selection.selectionChanged.connect(self.onSelectionChanged)
  238. root = controller.getScene().getRoot()
  239. self._platform = Platform(root)
  240. self._volume = BuildVolume.BuildVolume(root)
  241. self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
  242. self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume)
  243. camera = Camera("3d", root)
  244. camera.setPosition(Vector(-80, 250, 700))
  245. camera.setPerspective(True)
  246. camera.lookAt(Vector(0, 0, 0))
  247. controller.getScene().setActiveCamera("3d")
  248. self.getController().getTool("CameraTool").setOrigin(Vector(0, 100, 0))
  249. self._camera_animation = CameraAnimation.CameraAnimation()
  250. self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))
  251. self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
  252. qmlRegisterSingletonType(MachineManagerModel.MachineManagerModel, "Cura", 1, 0, "MachineManager",
  253. MachineManagerModel.createMachineManagerModel)
  254. self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
  255. self.initializeEngine()
  256. if self._engine.rootObjects:
  257. self.closeSplash()
  258. for file in self.getCommandLineOption("file", []):
  259. self._openFile(file)
  260. for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
  261. self._openFile(file_name)
  262. self.exec_()
  263. ## Handle Qt events
  264. def event(self, event):
  265. if event.type() == QEvent.FileOpen:
  266. if self._plugins_loaded:
  267. self._openFile(event.file())
  268. else:
  269. self._open_file_queue.append(event.file())
  270. return super().event(event)
  271. ## Get print information (duration / material used)
  272. def getPrintInformation(self):
  273. return self._print_information
  274. def registerObjects(self, engine):
  275. engine.rootContext().setContextProperty("Printer", self)
  276. self._print_information = PrintInformation.PrintInformation()
  277. engine.rootContext().setContextProperty("PrintInformation", self._print_information)
  278. self._cura_actions = CuraActions.CuraActions(self)
  279. engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
  280. qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
  281. def onSelectionChanged(self):
  282. if Selection.hasSelection():
  283. if not self.getController().getActiveTool():
  284. if self._previous_active_tool:
  285. self.getController().setActiveTool(self._previous_active_tool)
  286. self._previous_active_tool = None
  287. else:
  288. self.getController().setActiveTool("TranslateTool")
  289. if Preferences.getInstance().getValue("view/center_on_select"):
  290. self._center_after_select = True
  291. else:
  292. if self.getController().getActiveTool():
  293. self._previous_active_tool = self.getController().getActiveTool().getPluginId()
  294. self.getController().setActiveTool(None)
  295. else:
  296. self._previous_active_tool = None
  297. def _onToolOperationStopped(self, event):
  298. if self._center_after_select:
  299. self._center_after_select = False
  300. self._camera_animation.setStart(self.getController().getTool("CameraTool").getOrigin())
  301. self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
  302. self._camera_animation.start()
  303. requestAddPrinter = pyqtSignal()
  304. activityChanged = pyqtSignal()
  305. sceneBoundingBoxChanged = pyqtSignal()
  306. @pyqtProperty(bool, notify = activityChanged)
  307. def getPlatformActivity(self):
  308. return self._platform_activity
  309. @pyqtProperty(str, notify = sceneBoundingBoxChanged)
  310. def getSceneBoundingBoxString(self):
  311. return self._i18n_catalog.i18nc("@info", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_boundingbox.width.item(), 'depth': self._scene_boundingbox.depth.item(), 'height' : self._scene_boundingbox.height.item()}
  312. def updatePlatformActivity(self, node = None):
  313. count = 0
  314. scene_boundingbox = None
  315. for node in DepthFirstIterator(self.getController().getScene().getRoot()):
  316. if type(node) is not SceneNode or not node.getMeshData():
  317. continue
  318. count += 1
  319. if not scene_boundingbox:
  320. scene_boundingbox = copy.deepcopy(node.getBoundingBox())
  321. else:
  322. scene_boundingbox += node.getBoundingBox()
  323. if not scene_boundingbox:
  324. scene_boundingbox = AxisAlignedBox()
  325. if repr(self._scene_boundingbox) != repr(scene_boundingbox):
  326. self._scene_boundingbox = scene_boundingbox
  327. self.sceneBoundingBoxChanged.emit()
  328. self._platform_activity = True if count > 0 else False
  329. self.activityChanged.emit()
  330. @pyqtSlot(str)
  331. def setJobName(self, name):
  332. name = os.path.splitext(name)[0] #when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its extension. This cuts the extension off if nescessary.
  333. if self._job_name != name:
  334. self._job_name = name
  335. self.jobNameChanged.emit()
  336. jobNameChanged = pyqtSignal()
  337. @pyqtProperty(str, notify = jobNameChanged)
  338. def jobName(self):
  339. return self._job_name
  340. # Remove all selected objects from the scene.
  341. @pyqtSlot()
  342. def deleteSelection(self):
  343. if not self.getController().getToolsEnabled():
  344. return
  345. op = GroupedOperation()
  346. nodes = Selection.getAllSelectedObjects()
  347. for node in nodes:
  348. op.addOperation(RemoveSceneNodeOperation(node))
  349. op.push()
  350. pass
  351. ## Remove an object from the scene.
  352. # Note that this only removes an object if it is selected.
  353. @pyqtSlot("quint64")
  354. def deleteObject(self, object_id):
  355. if not self.getController().getToolsEnabled():
  356. return
  357. node = self.getController().getScene().findObject(object_id)
  358. if not node and object_id != 0: #Workaround for tool handles overlapping the selected object
  359. node = Selection.getSelectedObject(0)
  360. if node:
  361. if node.getParent():
  362. group_node = node.getParent()
  363. if not group_node.callDecoration("isGroup"):
  364. op = RemoveSceneNodeOperation(node)
  365. else:
  366. while group_node.getParent().callDecoration("isGroup"):
  367. group_node = group_node.getParent()
  368. op = RemoveSceneNodeOperation(group_node)
  369. op.push()
  370. ## Create a number of copies of existing object.
  371. @pyqtSlot("quint64", int)
  372. def multiplyObject(self, object_id, count):
  373. node = self.getController().getScene().findObject(object_id)
  374. if not node and object_id != 0: #Workaround for tool handles overlapping the selected object
  375. node = Selection.getSelectedObject(0)
  376. if node:
  377. op = GroupedOperation()
  378. for _ in range(count):
  379. if node.getParent() and node.getParent().callDecoration("isGroup"):
  380. new_node = copy.deepcopy(node.getParent()) #Copy the group node.
  381. new_node.callDecoration("setConvexHull",None)
  382. op.addOperation(AddSceneNodeOperation(new_node,node.getParent().getParent()))
  383. else:
  384. new_node = copy.deepcopy(node)
  385. new_node.callDecoration("setConvexHull", None)
  386. op.addOperation(AddSceneNodeOperation(new_node, node.getParent()))
  387. op.push()
  388. ## Center object on platform.
  389. @pyqtSlot("quint64")
  390. def centerObject(self, object_id):
  391. node = self.getController().getScene().findObject(object_id)
  392. if not node and object_id != 0: #Workaround for tool handles overlapping the selected object
  393. node = Selection.getSelectedObject(0)
  394. if not node:
  395. return
  396. if node.getParent() and node.getParent().callDecoration("isGroup"):
  397. node = node.getParent()
  398. if node:
  399. op = SetTransformOperation(node, Vector())
  400. op.push()
  401. ## Delete all mesh data on the scene.
  402. @pyqtSlot()
  403. def deleteAll(self):
  404. if not self.getController().getToolsEnabled():
  405. return
  406. nodes = []
  407. for node in DepthFirstIterator(self.getController().getScene().getRoot()):
  408. if type(node) is not SceneNode:
  409. continue
  410. if not node.getMeshData() and not node.callDecoration("isGroup"):
  411. continue #Node that doesnt have a mesh and is not a group.
  412. if node.getParent() and node.getParent().callDecoration("isGroup"):
  413. continue #Grouped nodes don't need resetting as their parent (the group) is resetted)
  414. nodes.append(node)
  415. if nodes:
  416. op = GroupedOperation()
  417. for node in nodes:
  418. op.addOperation(RemoveSceneNodeOperation(node))
  419. op.push()
  420. ## Reset all translation on nodes with mesh data.
  421. @pyqtSlot()
  422. def resetAllTranslation(self):
  423. nodes = []
  424. for node in DepthFirstIterator(self.getController().getScene().getRoot()):
  425. if type(node) is not SceneNode:
  426. continue
  427. if not node.getMeshData() and not node.callDecoration("isGroup"):
  428. continue #Node that doesnt have a mesh and is not a group.
  429. if node.getParent() and node.getParent().callDecoration("isGroup"):
  430. continue #Grouped nodes don't need resetting as their parent (the group) is resetted)
  431. nodes.append(node)
  432. if nodes:
  433. op = GroupedOperation()
  434. for node in nodes:
  435. node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  436. op.addOperation(SetTransformOperation(node, Vector(0,0,0)))
  437. op.push()
  438. ## Reset all transformations on nodes with mesh data.
  439. @pyqtSlot()
  440. def resetAll(self):
  441. nodes = []
  442. for node in DepthFirstIterator(self.getController().getScene().getRoot()):
  443. if type(node) is not SceneNode:
  444. continue
  445. if not node.getMeshData() and not node.callDecoration("isGroup"):
  446. continue #Node that doesnt have a mesh and is not a group.
  447. if node.getParent() and node.getParent().callDecoration("isGroup"):
  448. continue #Grouped nodes don't need resetting as their parent (the group) is resetted)
  449. nodes.append(node)
  450. if nodes:
  451. op = GroupedOperation()
  452. for node in nodes:
  453. # Ensure that the object is above the build platform
  454. node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  455. op.addOperation(SetTransformOperation(node, Vector(0,0,0), Quaternion(), Vector(1, 1, 1)))
  456. op.push()
  457. ## Reload all mesh data on the screen from file.
  458. @pyqtSlot()
  459. def reloadAll(self):
  460. nodes = []
  461. for node in DepthFirstIterator(self.getController().getScene().getRoot()):
  462. if type(node) is not SceneNode or not node.getMeshData():
  463. continue
  464. nodes.append(node)
  465. if not nodes:
  466. return
  467. for node in nodes:
  468. if not node.getMeshData():
  469. continue
  470. file_name = node.getMeshData().getFileName()
  471. if file_name:
  472. job = ReadMeshJob(file_name)
  473. job._node = node
  474. job.finished.connect(self._reloadMeshFinished)
  475. job.start()
  476. ## Get logging data of the backend engine
  477. # \returns \type{string} Logging data
  478. @pyqtSlot(result=str)
  479. def getEngineLog(self):
  480. log = ""
  481. for entry in self.getBackend().getLog():
  482. log += entry.decode()
  483. return log
  484. recentFilesChanged = pyqtSignal()
  485. @pyqtProperty("QVariantList", notify = recentFilesChanged)
  486. def recentFiles(self):
  487. return self._recent_files
  488. @pyqtSlot("QStringList")
  489. def setExpandedCategories(self, categories):
  490. categories = list(set(categories))
  491. categories.sort()
  492. joined = ";".join(categories)
  493. if joined != Preferences.getInstance().getValue("cura/categories_expanded"):
  494. Preferences.getInstance().setValue("cura/categories_expanded", joined)
  495. self.expandedCategoriesChanged.emit()
  496. expandedCategoriesChanged = pyqtSignal()
  497. @pyqtProperty("QStringList", notify = expandedCategoriesChanged)
  498. def expandedCategories(self):
  499. return Preferences.getInstance().getValue("cura/categories_expanded").split(";")
  500. @pyqtSlot()
  501. def mergeSelected(self):
  502. self.groupSelected()
  503. try:
  504. group_node = Selection.getAllSelectedObjects()[0]
  505. except Exception as e:
  506. Logger.log("d", "mergeSelected: Exception:", e)
  507. return
  508. multi_material_decorator = MultiMaterialDecorator.MultiMaterialDecorator()
  509. group_node.addDecorator(multi_material_decorator)
  510. # Reset the position of each node
  511. for node in group_node.getChildren():
  512. new_position = node.getMeshData().getCenterPosition()
  513. new_position = new_position.scale(node.getScale())
  514. node.setPosition(new_position)
  515. # Use the previously found center of the group bounding box as the new location of the group
  516. group_node.setPosition(group_node.getBoundingBox().center)
  517. @pyqtSlot()
  518. def groupSelected(self):
  519. group_node = SceneNode()
  520. group_decorator = GroupDecorator()
  521. group_node.addDecorator(group_decorator)
  522. group_node.setParent(self.getController().getScene().getRoot())
  523. group_node.setSelectable(True)
  524. center = Selection.getSelectionCenter()
  525. group_node.setPosition(center)
  526. group_node.setCenterPosition(center)
  527. for node in Selection.getAllSelectedObjects():
  528. world = node.getWorldPosition()
  529. node.setParent(group_node)
  530. node.setPosition(world - center)
  531. for node in group_node.getChildren():
  532. Selection.remove(node)
  533. Selection.add(group_node)
  534. @pyqtSlot()
  535. def ungroupSelected(self):
  536. ungrouped_nodes = []
  537. selected_objects = Selection.getAllSelectedObjects()[:] #clone the list
  538. for node in selected_objects:
  539. if node.callDecoration("isGroup" ):
  540. children_to_move = []
  541. for child in node.getChildren():
  542. if type(child) is SceneNode:
  543. children_to_move.append(child)
  544. for child in children_to_move:
  545. position = child.getWorldPosition()
  546. child.setParent(node.getParent())
  547. child.setPosition(position - node.getParent().getWorldPosition())
  548. child.scale(node.getScale())
  549. child.rotate(node.getOrientation())
  550. Selection.add(child)
  551. child.callDecoration("setConvexHull",None)
  552. node.setParent(None)
  553. ungrouped_nodes.append(node)
  554. for node in ungrouped_nodes:
  555. Selection.remove(node)
  556. def _createSplashScreen(self):
  557. return CuraSplashScreen.CuraSplashScreen()
  558. def _onActiveMachineChanged(self):
  559. pass
  560. def _onFileLoaded(self, job):
  561. node = job.getResult()
  562. if node != None:
  563. self.setJobName(os.path.basename(job.getFileName()))
  564. node.setSelectable(True)
  565. node.setName(os.path.basename(job.getFileName()))
  566. op = AddSceneNodeOperation(node, self.getController().getScene().getRoot())
  567. op.push()
  568. self.getController().getScene().sceneChanged.emit(node) #Force scene change.
  569. def _onJobFinished(self, job):
  570. if type(job) is not ReadMeshJob or not job.getResult():
  571. return
  572. f = QUrl.fromLocalFile(job.getFileName())
  573. if f in self._recent_files:
  574. self._recent_files.remove(f)
  575. self._recent_files.insert(0, f)
  576. if len(self._recent_files) > 10:
  577. del self._recent_files[10]
  578. pref = ""
  579. for path in self._recent_files:
  580. pref += path.toLocalFile() + ";"
  581. Preferences.getInstance().setValue("cura/recent_files", pref)
  582. self.recentFilesChanged.emit()
  583. def _reloadMeshFinished(self, job):
  584. # TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
  585. job._node.setMeshData(job.getResult().getMeshData())
  586. #job.getResult().setParent(self.getController().getScene().getRoot())
  587. #job._node.setParent(self.getController().getScene().getRoot())
  588. #job._node.meshDataChanged.emit(job._node)
  589. def _openFile(self, file):
  590. job = ReadMeshJob(os.path.abspath(file))
  591. job.finished.connect(self._onFileLoaded)
  592. job.start()
  593. def _addProfileReader(self, profile_reader):
  594. # TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
  595. pass