CuraEngineBackend.py 43 KB

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