CuraEngineBackend.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.Backend.Backend import Backend, BackendState
  4. from UM.Application import Application
  5. from UM.Scene.SceneNode import SceneNode
  6. from UM.Preferences import Preferences
  7. from UM.Signal import Signal
  8. from UM.Logger import Logger
  9. from UM.Message import Message
  10. from UM.PluginRegistry import PluginRegistry
  11. from UM.Resources import Resources
  12. from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
  13. from UM.Platform import Platform
  14. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  15. from UM.Qt.Duration import DurationFormat
  16. from PyQt5.QtCore import QObject, pyqtSlot
  17. from cura.Settings.ExtruderManager import ExtruderManager
  18. from . import ProcessSlicedLayersJob
  19. from . import StartSliceJob
  20. import os
  21. import sys
  22. from time import time
  23. from PyQt5.QtCore import QTimer
  24. import Arcus
  25. from UM.i18n import i18nCatalog
  26. catalog = i18nCatalog("cura")
  27. class CuraEngineBackend(QObject, Backend):
  28. ## Starts the back-end plug-in.
  29. #
  30. # This registers all the signal listeners and prepares for communication
  31. # with the back-end in general.
  32. # CuraEngineBackend is exposed to qml as well.
  33. def __init__(self, parent = None):
  34. super().__init__(parent = parent)
  35. # Find out where the engine is located, and how it is called.
  36. # This depends on how Cura is packaged and which OS we are running on.
  37. executable_name = "CuraEngine"
  38. if Platform.isWindows():
  39. executable_name += ".exe"
  40. default_engine_location = executable_name
  41. if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)):
  42. default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name)
  43. if hasattr(sys, "frozen"):
  44. default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
  45. if Platform.isLinux() and not default_engine_location:
  46. if not os.getenv("PATH"):
  47. raise OSError("There is something wrong with your Linux installation.")
  48. for pathdir in os.getenv("PATH").split(os.pathsep):
  49. execpath = os.path.join(pathdir, executable_name)
  50. if os.path.exists(execpath):
  51. default_engine_location = execpath
  52. break
  53. if not default_engine_location:
  54. raise EnvironmentError("Could not find CuraEngine")
  55. Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
  56. default_engine_location = os.path.abspath(default_engine_location)
  57. Preferences.getInstance().addPreference("backend/location", default_engine_location)
  58. # Workaround to disable layer view processing if layer view is not active.
  59. self._layer_view_active = False
  60. Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
  61. Application.getInstance().activeBuildPlateChanged.connect(self._onActiveViewChanged)
  62. self._onActiveViewChanged()
  63. self._stored_layer_data = []
  64. self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
  65. self._scene = Application.getInstance().getController().getScene()
  66. self._scene.sceneChanged.connect(self._onSceneChanged)
  67. # Triggers for auto-slicing. Auto-slicing is triggered as follows:
  68. # - auto-slicing is started with a timer
  69. # - whenever there is a value change, we start the timer
  70. # - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
  71. # auto-slicing timer when that error check is finished
  72. # If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer,
  73. # and only wait for the error check to be finished to start the auto-slicing timer again.
  74. #
  75. self._global_container_stack = None
  76. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
  77. self._onGlobalStackChanged()
  78. Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
  79. # A flag indicating if an error check was scheduled
  80. # If so, we will stop the auto-slice timer and start upon the error check
  81. self._is_error_check_scheduled = False
  82. # Listeners for receiving messages from the back-end.
  83. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
  84. self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage
  85. self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
  86. self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
  87. self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
  88. self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
  89. self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
  90. self._start_slice_job = None
  91. self._start_slice_job_build_plate = None
  92. self._slicing = False # Are we currently slicing?
  93. self._restart = False # Back-end is currently restarting?
  94. self._tool_active = False # If a tool is active, some tasks do not have to do anything
  95. self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
  96. self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
  97. # self._need_slicing = False
  98. self._build_plates_to_be_sliced = [] # what needs slicing?
  99. self._engine_is_fresh = True # Is the newly started engine used before or not?
  100. self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
  101. self._error_message = None # Pop-up message that shows errors.
  102. self._last_num_objects = 0 # Count number of objects to see if there is something changed
  103. self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
  104. self.backendQuit.connect(self._onBackendQuit)
  105. self.backendConnected.connect(self._onBackendConnected)
  106. # When a tool operation is in progress, don't slice. So we need to listen for tool operations.
  107. Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
  108. Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
  109. self._slice_start_time = None
  110. Preferences.getInstance().addPreference("general/auto_slice", True)
  111. self._use_timer = False
  112. # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
  113. # This timer will group them up, and only slice for the last setting changed signal.
  114. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
  115. self._change_timer = QTimer()
  116. self._change_timer.setSingleShot(True)
  117. self._change_timer.setInterval(500)
  118. self.determineAutoSlicing()
  119. Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
  120. ## Terminate the engine process.
  121. #
  122. # This function should terminate the engine process.
  123. # Called when closing the application.
  124. def close(self):
  125. # Terminate CuraEngine if it is still running at this point
  126. self._terminate()
  127. ## Get the command that is used to call the engine.
  128. # This is useful for debugging and used to actually start the engine.
  129. # \return list of commands and args / parameters.
  130. def getEngineCommand(self):
  131. json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
  132. return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
  133. ## Emitted when we get a message containing print duration and material amount.
  134. # This also implies the slicing has finished.
  135. # \param time The amount of time the print will take.
  136. # \param material_amount The amount of material the print will use.
  137. printDurationMessage = Signal()
  138. ## Emitted when the slicing process starts.
  139. slicingStarted = Signal()
  140. ## Emitted when the slicing process is aborted forcefully.
  141. slicingCancelled = Signal()
  142. @pyqtSlot()
  143. def stopSlicing(self):
  144. self.backendStateChange.emit(BackendState.NotStarted)
  145. if self._slicing: # We were already slicing. Stop the old job.
  146. self._terminate()
  147. self._createSocket()
  148. if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
  149. self._process_layers_job.abort()
  150. self._process_layers_job = None
  151. if self._error_message:
  152. self._error_message.hide()
  153. ## Manually triggers a reslice
  154. @pyqtSlot()
  155. def forceSlice(self):
  156. if self._use_timer:
  157. self._change_timer.start()
  158. else:
  159. self.slice()
  160. ## Perform a slice of the scene.
  161. def slice(self):
  162. Logger.log("d", "starting to slice again!")
  163. self._slice_start_time = time()
  164. if not self._build_plates_to_be_sliced:
  165. self.processingProgress.emit(1.0)
  166. self.backendStateChange.emit(BackendState.Done)
  167. Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
  168. return
  169. if Application.getInstance().getPrintInformation():
  170. Application.getInstance().getPrintInformation().setToZeroPrintInformation()
  171. self._stored_layer_data = []
  172. if self._process is None:
  173. self._createSocket()
  174. self.stopSlicing()
  175. self._engine_is_fresh = False # Yes we're going to use the engine
  176. self.processingProgress.emit(0.0)
  177. self.backendStateChange.emit(BackendState.NotStarted)
  178. self._scene.gcode_list = []
  179. self._slicing = True
  180. self.slicingStarted.emit()
  181. slice_message = self._socket.createMessage("cura.proto.Slice")
  182. self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
  183. self._start_slice_job_build_plate = self._build_plates_to_be_sliced.pop(0)
  184. self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
  185. self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
  186. self._start_slice_job.start()
  187. self._start_slice_job.finished.connect(self._onStartSliceCompleted)
  188. ## Terminate the engine process.
  189. # Start the engine process by calling _createSocket()
  190. def _terminate(self):
  191. self._slicing = False
  192. self._stored_layer_data = []
  193. if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
  194. del self._stored_optimized_layer_data[self._start_slice_job_build_plate]
  195. if self._start_slice_job is not None:
  196. self._start_slice_job.cancel()
  197. self.slicingCancelled.emit()
  198. self.processingProgress.emit(0)
  199. Logger.log("d", "Attempting to kill the engine process")
  200. if Application.getInstance().getCommandLineOption("external-backend", False):
  201. return
  202. if self._process is not None:
  203. Logger.log("d", "Killing engine process")
  204. try:
  205. self._process.terminate()
  206. Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
  207. self._process = None
  208. except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
  209. Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
  210. ## Event handler to call when the job to initiate the slicing process is
  211. # completed.
  212. #
  213. # When the start slice job is successfully completed, it will be happily
  214. # slicing. This function handles any errors that may occur during the
  215. # bootstrapping of a slice job.
  216. #
  217. # \param job The start slice job that was just finished.
  218. def _onStartSliceCompleted(self, job):
  219. if self._error_message:
  220. self._error_message.hide()
  221. # Note that cancelled slice jobs can still call this method.
  222. if self._start_slice_job is job:
  223. self._start_slice_job = None
  224. if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
  225. return
  226. if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
  227. if Application.getInstance().platformActivity:
  228. self._error_message = Message(catalog.i18nc("@info:status",
  229. "Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
  230. self._error_message.show()
  231. self.backendStateChange.emit(BackendState.Error)
  232. else:
  233. self.backendStateChange.emit(BackendState.NotStarted)
  234. return
  235. if job.getResult() == StartSliceJob.StartJobResult.SettingError:
  236. if Application.getInstance().platformActivity:
  237. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  238. error_keys = []
  239. for extruder in extruders:
  240. error_keys.extend(extruder.getErrorKeys())
  241. if not extruders:
  242. error_keys = self._global_container_stack.getErrorKeys()
  243. error_labels = set()
  244. for key in error_keys:
  245. for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
  246. definitions = stack.getBottom().findDefinitions(key = key)
  247. if definitions:
  248. break #Found it! No need to continue search.
  249. else: #No stack has a definition for this setting.
  250. Logger.log("w", "When checking settings for errors, unable to find definition for key: {key}".format(key = key))
  251. continue
  252. error_labels.add(definitions[0].label)
  253. error_labels = ", ".join(error_labels)
  254. self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}".format(error_labels)),
  255. title = catalog.i18nc("@info:title", "Unable to slice"))
  256. self._error_message.show()
  257. self.backendStateChange.emit(BackendState.Error)
  258. else:
  259. self.backendStateChange.emit(BackendState.NotStarted)
  260. return
  261. if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
  262. if Application.getInstance().platformActivity:
  263. self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
  264. title = catalog.i18nc("@info:title", "Unable to slice"))
  265. self._error_message.show()
  266. self.backendStateChange.emit(BackendState.Error)
  267. else:
  268. self.backendStateChange.emit(BackendState.NotStarted)
  269. if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
  270. if Application.getInstance().platformActivity:
  271. self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
  272. title = catalog.i18nc("@info:title", "Unable to slice"))
  273. self._error_message.show()
  274. #self.backendStateChange.emit(BackendState.Error)
  275. else:
  276. #self.backendStateChange.emit(BackendState.NotStarted)
  277. pass
  278. self._invokeSlice()
  279. return
  280. # Preparation completed, send it to the backend.
  281. self._socket.sendMessage(job.getSliceMessage())
  282. # Notify the user that it's now up to the backend to do it's job
  283. self.backendStateChange.emit(BackendState.Processing)
  284. Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
  285. ## Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
  286. # It disables when
  287. # - preference auto slice is off
  288. # - decorator isBlockSlicing is found (used in g-code reader)
  289. def determineAutoSlicing(self):
  290. enable_timer = True
  291. if not Preferences.getInstance().getValue("general/auto_slice"):
  292. enable_timer = False
  293. for node in DepthFirstIterator(self._scene.getRoot()):
  294. if node.callDecoration("isBlockSlicing"):
  295. enable_timer = False
  296. self.backendStateChange.emit(BackendState.Disabled)
  297. gcode_list = node.callDecoration("getGCodeList")
  298. if gcode_list is not None:
  299. self._scene.gcode_list = gcode_list
  300. if self._use_timer == enable_timer:
  301. return self._use_timer
  302. if enable_timer:
  303. self.backendStateChange.emit(BackendState.NotStarted)
  304. self.enableTimer()
  305. return True
  306. else:
  307. self.disableTimer()
  308. return False
  309. ## Listener for when the scene has changed.
  310. #
  311. # This should start a slice if the scene is now ready to slice.
  312. #
  313. # \param source The scene node that was changed.
  314. def _onSceneChanged(self, source):
  315. Logger.log("d", " ##### scene changed: %s", source)
  316. if not issubclass(type(source), SceneNode):
  317. return
  318. root_scene_nodes_changed = False
  319. build_plates_changed = set()
  320. if source == self._scene.getRoot():
  321. num_objects = 0
  322. for node in DepthFirstIterator(self._scene.getRoot()):
  323. # Only count sliceable objects
  324. if node.callDecoration("isSliceable"):
  325. num_objects += 1
  326. build_plates_changed.add(node.callDecoration("getBuildPlateNumber"))
  327. build_plates_changed.add(node.callDecoration("getPreviousBuildPlateNumber"))
  328. if num_objects != self._last_num_objects:
  329. self._last_num_objects = num_objects
  330. root_scene_nodes_changed = True
  331. # else:
  332. # return # ??
  333. build_plates_changed.discard(None)
  334. build_plates_changed.discard(-1) # object not on build plate
  335. Logger.log("d", " #### build plates changed: %s", build_plates_changed)
  336. # if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
  337. # if source.getMeshData() is None:
  338. # return
  339. # if source.getMeshData().getVertices() is None:
  340. # return
  341. if self._tool_active:
  342. # do it later, each source only has to be done once
  343. if source not in self._postponed_scene_change_sources:
  344. self._postponed_scene_change_sources.append(source)
  345. return
  346. if build_plates_changed:
  347. Logger.log("d", " going to reslice")
  348. self.stopSlicing()
  349. for build_plate_number in build_plates_changed:
  350. if build_plate_number not in self._build_plates_to_be_sliced:
  351. self._build_plates_to_be_sliced.append(build_plate_number)
  352. self.processingProgress.emit(0.0)
  353. self.backendStateChange.emit(BackendState.NotStarted)
  354. if not self._use_timer:
  355. # With manually having to slice, we want to clear the old invalid layer data.
  356. self._clearLayerData(build_plates_changed)
  357. self._invokeSlice()
  358. # #self.needsSlicing()
  359. # self.stopSlicing()
  360. # #self._onChanged()
  361. # self._invokeSlice()
  362. ## Called when an error occurs in the socket connection towards the engine.
  363. #
  364. # \param error The exception that occurred.
  365. def _onSocketError(self, error):
  366. if Application.getInstance().isShuttingDown():
  367. return
  368. super()._onSocketError(error)
  369. if error.getErrorCode() == Arcus.ErrorCode.Debug:
  370. return
  371. self._terminate()
  372. self._createSocket()
  373. if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
  374. Logger.log("w", "A socket error caused the connection to be reset")
  375. ## Remove old layer data (if any)
  376. def _clearLayerData(self, build_plate_numbers = set()):
  377. for node in DepthFirstIterator(self._scene.getRoot()):
  378. if node.callDecoration("getLayerData"):
  379. if node.callDecoration("getBuildPlateNumber") in build_plate_numbers or not build_plate_numbers:
  380. node.getParent().removeChild(node)
  381. def markSliceAll(self):
  382. if 0 not in self._build_plates_to_be_sliced:
  383. self._build_plates_to_be_sliced.append(0)
  384. if 1 not in self._build_plates_to_be_sliced:
  385. self._build_plates_to_be_sliced.append(1)
  386. if 2 not in self._build_plates_to_be_sliced:
  387. self._build_plates_to_be_sliced.append(2)
  388. ## Convenient function: mark everything to slice, emit state and clear layer data
  389. def needsSlicing(self):
  390. self.stopSlicing()
  391. self.markSliceAll()
  392. self.processingProgress.emit(0.0)
  393. self.backendStateChange.emit(BackendState.NotStarted)
  394. if not self._use_timer:
  395. # With manually having to slice, we want to clear the old invalid layer data.
  396. self._clearLayerData()
  397. ## A setting has changed, so check if we must reslice.
  398. # \param instance The setting instance that has changed.
  399. # \param property The property of the setting instance that has changed.
  400. def _onSettingChanged(self, instance, property):
  401. if property == "value": # Only reslice if the value has changed.
  402. self.needsSlicing()
  403. self._onChanged()
  404. elif property == "validationState":
  405. if self._use_timer:
  406. self._is_error_check_scheduled = True
  407. self._change_timer.stop()
  408. def _onStackErrorCheckFinished(self):
  409. self._is_error_check_scheduled = False
  410. if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing:
  411. self.needsSlicing()
  412. self._onChanged()
  413. ## Called when a sliced layer data message is received from the engine.
  414. #
  415. # \param message The protobuf message containing sliced layer data.
  416. def _onLayerMessage(self, message):
  417. self._stored_layer_data.append(message)
  418. ## Called when an optimized sliced layer data message is received from the engine.
  419. #
  420. # \param message The protobuf message containing sliced layer data.
  421. def _onOptimizedLayerMessage(self, message):
  422. self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
  423. ## Called when a progress message is received from the engine.
  424. #
  425. # \param message The protobuf message containing the slicing progress.
  426. def _onProgressMessage(self, message):
  427. self.processingProgress.emit(message.amount)
  428. self.backendStateChange.emit(BackendState.Processing)
  429. # testing
  430. def _invokeSlice(self):
  431. if self._use_timer:
  432. # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
  433. # otherwise business as usual
  434. if self._is_error_check_scheduled:
  435. self._change_timer.stop()
  436. else:
  437. self._change_timer.start()
  438. ## Called when the engine sends a message that slicing is finished.
  439. #
  440. # \param message The protobuf message signalling that slicing is finished.
  441. def _onSlicingFinishedMessage(self, message):
  442. self.backendStateChange.emit(BackendState.Done)
  443. self.processingProgress.emit(1.0)
  444. for line in self._scene.gcode_list:
  445. replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
  446. replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
  447. replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights))
  448. replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts))
  449. replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName))
  450. self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced
  451. self._slicing = False
  452. #self._need_slicing = False
  453. Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
  454. # See if we need to process the sliced layers job.
  455. active_build_plate = Application.getInstance().activeBuildPlate
  456. if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
  457. self._startProcessSlicedLayersJob(active_build_plate)
  458. self._start_slice_job_build_plate = None
  459. Logger.log("d", "See if there is more to slice...")
  460. # Somehow this results in an Arcus Error
  461. # self.slice()
  462. # Testing call slice again, allow backend to restart by using the timer
  463. self._invokeSlice()
  464. ## Called when a g-code message is received from the engine.
  465. #
  466. # \param message The protobuf message containing g-code, encoded as UTF-8.
  467. def _onGCodeLayerMessage(self, message):
  468. self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))
  469. ## Called when a g-code prefix message is received from the engine.
  470. #
  471. # \param message The protobuf message containing the g-code prefix,
  472. # encoded as UTF-8.
  473. def _onGCodePrefixMessage(self, message):
  474. self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
  475. ## Creates a new socket connection.
  476. def _createSocket(self):
  477. super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
  478. self._engine_is_fresh = True
  479. ## Called when anything has changed to the stuff that needs to be sliced.
  480. #
  481. # This indicates that we should probably re-slice soon.
  482. def _onChanged(self, *args, **kwargs):
  483. self.needsSlicing()
  484. if self._use_timer:
  485. # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
  486. # otherwise business as usual
  487. if self._is_error_check_scheduled:
  488. self._change_timer.stop()
  489. else:
  490. self._change_timer.start()
  491. ## Called when a print time message is received from the engine.
  492. #
  493. # \param message The protobuf message containing the print time per feature and
  494. # material amount per extruder
  495. def _onPrintTimeMaterialEstimates(self, message):
  496. material_amounts = []
  497. for index in range(message.repeatedMessageCount("materialEstimates")):
  498. material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
  499. times = self._parseMessagePrintTimes(message)
  500. self.printDurationMessage.emit(times, material_amounts)
  501. ## Called for parsing message to retrieve estimated time per feature
  502. #
  503. # \param message The protobuf message containing the print time per feature
  504. def _parseMessagePrintTimes(self, message):
  505. result = {
  506. "inset_0": message.time_inset_0,
  507. "inset_x": message.time_inset_x,
  508. "skin": message.time_skin,
  509. "infill": message.time_infill,
  510. "support_infill": message.time_support_infill,
  511. "support_interface": message.time_support_interface,
  512. "support": message.time_support,
  513. "skirt": message.time_skirt,
  514. "travel": message.time_travel,
  515. "retract": message.time_retract,
  516. "none": message.time_none
  517. }
  518. return result
  519. ## Called when the back-end connects to the front-end.
  520. def _onBackendConnected(self):
  521. if self._restart:
  522. self._restart = False
  523. self._onChanged()
  524. ## Called when the user starts using some tool.
  525. #
  526. # When the user starts using a tool, we should pause slicing to prevent
  527. # continuously slicing while the user is dragging some tool handle.
  528. #
  529. # \param tool The tool that the user is using.
  530. def _onToolOperationStarted(self, tool):
  531. self._tool_active = True # Do not react on scene change
  532. self.disableTimer()
  533. # Restart engine as soon as possible, we know we want to slice afterwards
  534. if not self._engine_is_fresh:
  535. self._terminate()
  536. self._createSocket()
  537. ## Called when the user stops using some tool.
  538. #
  539. # This indicates that we can safely start slicing again.
  540. #
  541. # \param tool The tool that the user was using.
  542. def _onToolOperationStopped(self, tool):
  543. self._tool_active = False # React on scene change again
  544. self.determineAutoSlicing() # Switch timer on if appropriate
  545. # Process all the postponed scene changes
  546. while self._postponed_scene_change_sources:
  547. source = self._postponed_scene_change_sources.pop(0)
  548. self._onSceneChanged(source)
  549. def _startProcessSlicedLayersJob(self, build_plate_number):
  550. self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
  551. self._process_layers_job.setBuildPlate(build_plate_number)
  552. self._process_layers_job.finished.connect(self._onProcessLayersFinished)
  553. self._process_layers_job.start()
  554. del self._stored_optimized_layer_data[build_plate_number]
  555. ## Called when the user changes the active view mode.
  556. def _onActiveViewChanged(self):
  557. application = Application.getInstance()
  558. view = application.getController().getActiveView()
  559. if view:
  560. active_build_plate = application.activeBuildPlate
  561. if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet.
  562. self._layer_view_active = True
  563. # There is data and we're not slicing at the moment
  564. # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
  565. # TODO: what build plate I am slicing
  566. if active_build_plate in self._stored_optimized_layer_data and not self._slicing:
  567. self._startProcessSlicedLayersJob(active_build_plate)
  568. else:
  569. self._layer_view_active = False
  570. ## Called when the back-end self-terminates.
  571. #
  572. # We should reset our state and start listening for new connections.
  573. def _onBackendQuit(self):
  574. if not self._restart:
  575. if self._process:
  576. Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
  577. self._process = None
  578. ## Called when the global container stack changes
  579. def _onGlobalStackChanged(self):
  580. if self._global_container_stack:
  581. self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
  582. self._global_container_stack.containersChanged.disconnect(self._onChanged)
  583. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  584. for extruder in extruders:
  585. extruder.propertyChanged.disconnect(self._onSettingChanged)
  586. extruder.containersChanged.disconnect(self._onChanged)
  587. self._global_container_stack = Application.getInstance().getGlobalContainerStack()
  588. if self._global_container_stack:
  589. self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
  590. self._global_container_stack.containersChanged.connect(self._onChanged)
  591. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  592. for extruder in extruders:
  593. extruder.propertyChanged.connect(self._onSettingChanged)
  594. extruder.containersChanged.connect(self._onChanged)
  595. self._onChanged()
  596. def _onProcessLayersFinished(self, job):
  597. self._process_layers_job = None
  598. ## Connect slice function to timer.
  599. def enableTimer(self):
  600. if not self._use_timer:
  601. self._change_timer.timeout.connect(self.slice)
  602. self._use_timer = True
  603. ## Disconnect slice function from timer.
  604. # This means that slicing will not be triggered automatically
  605. def disableTimer(self):
  606. if self._use_timer:
  607. self._use_timer = False
  608. self._change_timer.timeout.disconnect(self.slice)
  609. def _onPreferencesChanged(self, preference):
  610. if preference != "general/auto_slice":
  611. return
  612. auto_slice = self.determineAutoSlicing()
  613. if auto_slice:
  614. self._change_timer.start()
  615. ## Tickle the backend so in case of auto slicing, it starts the timer.
  616. def tickle(self):
  617. if self._use_timer:
  618. self._change_timer.start()