CuraEngineBackend.py 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151
  1. # Copyright (c) 2022 UltiMaker
  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 PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSlot
  7. import sys
  8. from time import time
  9. from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
  10. from PyQt6.QtGui import QDesktopServices, QImage
  11. from UM.Backend.Backend import Backend, BackendState
  12. from UM.Scene.SceneNode import SceneNode
  13. from UM.Signal import Signal
  14. from UM.Logger import Logger
  15. from UM.Message import Message
  16. from UM.PluginRegistry import PluginRegistry
  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 cura.CuraApplication import CuraApplication
  24. from cura.Settings.ExtruderManager import ExtruderManager
  25. from cura.Snapshot import Snapshot
  26. from cura.Utils.Threading import call_on_qt_thread
  27. from .ProcessSlicedLayersJob import ProcessSlicedLayersJob
  28. from .StartSliceJob import StartSliceJob, StartJobResult
  29. import pyArcus as Arcus
  30. if TYPE_CHECKING:
  31. from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
  32. from cura.Machines.MachineErrorChecker import MachineErrorChecker
  33. from UM.Scene.Scene import Scene
  34. from UM.Settings.ContainerStack import ContainerStack
  35. from UM.i18n import i18nCatalog
  36. catalog = i18nCatalog("cura")
  37. class CuraEngineBackend(QObject, Backend):
  38. backendError = Signal()
  39. printDurationMessage = Signal()
  40. """Emitted when we get a message containing print duration and material amount.
  41. This also implies the slicing has finished.
  42. :param time: The amount of time the print will take.
  43. :param material_amount: The amount of material the print will use.
  44. """
  45. slicingStarted = Signal()
  46. """Emitted when the slicing process starts."""
  47. slicingCancelled = Signal()
  48. """Emitted when the slicing process is aborted forcefully."""
  49. def __init__(self) -> None:
  50. """Starts the back-end plug-in.
  51. This registers all the signal listeners and prepares for communication
  52. with the back-end in general.
  53. CuraEngineBackend is exposed to qml as well.
  54. """
  55. super().__init__()
  56. self._init_done = False
  57. self._immediate_slice_after_init = False
  58. # Find out where the engine is located, and how it is called.
  59. # This depends on how Cura is packaged and which OS we are running on.
  60. executable_name = "CuraEngine"
  61. if Platform.isWindows():
  62. executable_name += ".exe"
  63. self._default_engine_location = executable_name
  64. search_path = [
  65. os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..", "Resources")),
  66. os.path.abspath(os.path.dirname(sys.executable)),
  67. os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
  68. os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
  69. os.path.join(CuraApplication.getInstallPrefix(), "bin"),
  70. os.path.dirname(os.path.abspath(sys.executable)),
  71. ]
  72. for path in search_path:
  73. engine_path = os.path.join(path, executable_name)
  74. if os.path.isfile(engine_path):
  75. self._default_engine_location = engine_path
  76. break
  77. if Platform.isLinux() and not self._default_engine_location:
  78. if not os.getenv("PATH"):
  79. raise OSError("There is something wrong with your Linux installation.")
  80. for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
  81. execpath = os.path.join(pathdir, executable_name)
  82. if os.path.exists(execpath):
  83. self._default_engine_location = execpath
  84. break
  85. application: CuraApplication = CuraApplication.getInstance()
  86. self._multi_build_plate_model: Optional[MultiBuildPlateModel] = None
  87. self._machine_error_checker: Optional[MachineErrorChecker] = None
  88. if not self._default_engine_location:
  89. raise EnvironmentError("Could not find CuraEngine")
  90. Logger.log("i", "Found CuraEngine at: %s", self._default_engine_location)
  91. self._default_engine_location = os.path.abspath(self._default_engine_location)
  92. application.getPreferences().addPreference("backend/location", self._default_engine_location)
  93. # Workaround to disable layer view processing if layer view is not active.
  94. self._layer_view_active: bool = False
  95. self._onActiveViewChanged()
  96. self._stored_layer_data: List[Arcus.PythonMessage] = []
  97. # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
  98. self._stored_optimized_layer_data: Dict[int, List[Arcus.PythonMessage]] = {}
  99. self._scene: Scene = application.getController().getScene()
  100. self._scene.sceneChanged.connect(self._onSceneChanged)
  101. # Triggers for auto-slicing. Auto-slicing is triggered as follows:
  102. # - auto-slicing is started with a timer
  103. # - whenever there is a value change, we start the timer
  104. # - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
  105. # auto-slicing timer when that error check is finished
  106. # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
  107. # to start the auto-slicing timer again.
  108. #
  109. self._global_container_stack: Optional[ContainerStack] = None
  110. # Listeners for receiving messages from the back-end.
  111. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
  112. self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage
  113. self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
  114. self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
  115. self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
  116. self._message_handlers["cura.proto.SliceUUID"] = self._onSliceUUIDMessage
  117. self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
  118. self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
  119. self._start_slice_job: Optional[StartSliceJob] = None
  120. self._start_slice_job_build_plate: Optional[int] = None
  121. self._slicing: bool = False # Are we currently slicing?
  122. self._restart: bool = False # Back-end is currently restarting?
  123. self._tool_active: bool = False # If a tool is active, some tasks do not have to do anything
  124. self._always_restart: bool = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
  125. self._process_layers_job: Optional[ProcessSlicedLayersJob] = None # The currently active job to process layers, or None if it is not processing layers.
  126. self._build_plates_to_be_sliced: List[int] = [] # what needs slicing?
  127. self._engine_is_fresh: bool = True # Is the newly started engine used before or not?
  128. self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer
  129. self._error_message: Optional[Message] = None # Pop-up message that shows errors.
  130. # Count number of objects to see if there is something changed
  131. self._last_num_objects: Dict[int, int] = defaultdict(int)
  132. self._postponed_scene_change_sources: List[SceneNode] = [] # scene change is postponed (by a tool)
  133. self._time_start_process: Optional[float] = None
  134. self._is_disabled: bool = False
  135. application.getPreferences().addPreference("general/auto_slice", False)
  136. application.getPreferences().addPreference("info/send_engine_crash", True)
  137. application.getPreferences().addPreference("info/anonymous_engine_crash_report", True)
  138. self._use_timer: bool = False
  139. # When you update a setting and other settings get changed through inheritance, many propertyChanged
  140. # signals are fired. This timer will group them up, and only slice for the last setting changed signal.
  141. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
  142. self._change_timer: QTimer = QTimer()
  143. self._change_timer.setSingleShot(True)
  144. self._change_timer.setInterval(500)
  145. self.determineAutoSlicing()
  146. application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
  147. self._slicing_error_message = Message(
  148. text = catalog.i18nc("@message", "Oops! We encountered an unexpected error during your slicing process. "
  149. "Rest assured, we've automatically received the crash logs for analysis, "
  150. "if you have not disabled data sharing in your preferences. To assist us "
  151. "further, consider sharing your project details on our issue tracker."),
  152. title = catalog.i18nc("@message:title", "Slicing failed"),
  153. message_type = Message.MessageType.ERROR
  154. )
  155. self._slicing_error_message.addAction(
  156. action_id = "report_bug",
  157. name = catalog.i18nc("@message:button", "Report a bug"),
  158. description = catalog.i18nc("@message:description", "Report a bug on UltiMaker Cura's issue tracker."),
  159. icon = "[no_icon]"
  160. )
  161. self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
  162. self._resetLastSliceTimeStats()
  163. self._snapshot: Optional[QImage] = None
  164. self._last_socket_error: Optional[Arcus.Error] = None
  165. application.initializationFinished.connect(self.initialize)
  166. # Ensure that the initial value for send_engine_crash is handled correctly.
  167. application.callLater(self._onPreferencesChanged, "info/send_engine_crash")
  168. def startPlugins(self) -> None:
  169. """
  170. Ensure that all backend plugins are started
  171. It assigns unique ports to each plugin to avoid conflicts.
  172. :return:
  173. """
  174. self.stopPlugins()
  175. backend_plugins = CuraApplication.getInstance().getBackendPlugins()
  176. for backend_plugin in backend_plugins:
  177. # Set the port to prevent plugins from using the same one.
  178. if backend_plugin.getPort() < 1:
  179. backend_plugin.setAvailablePort()
  180. backend_plugin.start()
  181. def stopPlugins(self) -> None:
  182. """
  183. Ensure that all backend plugins will be terminated.
  184. """
  185. backend_plugins = CuraApplication.getInstance().getBackendPlugins()
  186. for backend_plugin in backend_plugins:
  187. if backend_plugin.isRunning():
  188. backend_plugin.stop()
  189. def _resetLastSliceTimeStats(self) -> None:
  190. self._time_start_process = None
  191. self._time_send_message = None
  192. self._time_end_slice = None
  193. def resetAndReturnLastSliceTimeStats(self) -> Dict[str, float]:
  194. last_slice_data = {
  195. "time_start_process": self._time_start_process,
  196. "time_send_message": self._time_send_message,
  197. "time_end_slice": self._time_end_slice,
  198. }
  199. self._resetLastSliceTimeStats()
  200. return last_slice_data
  201. def initialize(self) -> None:
  202. application = CuraApplication.getInstance()
  203. self._multi_build_plate_model = application.getMultiBuildPlateModel()
  204. application.getController().activeViewChanged.connect(self._onActiveViewChanged)
  205. if self._multi_build_plate_model:
  206. self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
  207. application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
  208. self._onGlobalStackChanged()
  209. # Extruder enable / disable. Actually wanted to use machine manager here,
  210. # but the initialization order causes it to crash
  211. ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
  212. self.backendQuit.connect(self._onBackendQuit)
  213. self.backendConnected.connect(self._onBackendConnected)
  214. # When a tool operation is in progress, don't slice. So we need to listen for tool operations.
  215. application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
  216. application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
  217. self._machine_error_checker = application.getMachineErrorChecker()
  218. self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
  219. self._init_done = True
  220. if self._immediate_slice_after_init:
  221. self.slice()
  222. def close(self) -> None:
  223. """Terminate the engine process.
  224. This function should terminate the engine process.
  225. Called when closing the application.
  226. """
  227. # Terminate CuraEngine if it is still running at this point
  228. self._terminate()
  229. def getEngineCommand(self) -> List[str]:
  230. """Get the command that is used to call the engine.
  231. This is useful for debugging and used to actually start the engine.
  232. :return: list of commands and args / parameters.
  233. """
  234. from cura import ApplicationMetadata
  235. if ApplicationMetadata.IsEnterpriseVersion:
  236. command = [self._default_engine_location]
  237. else:
  238. command = [CuraApplication.getInstance().getPreferences().getValue("backend/location")]
  239. command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
  240. parser = argparse.ArgumentParser(prog = "cura", add_help = False)
  241. parser.add_argument("--debug", action = "store_true", default = False,
  242. help = "Turn on the debug mode by setting this option.")
  243. known_args = vars(parser.parse_known_args()[0])
  244. if known_args["debug"]:
  245. command.append("-vvv")
  246. return command
  247. @pyqtSlot()
  248. def stopSlicing(self) -> None:
  249. self.setState(BackendState.NotStarted)
  250. if self._slicing: # We were already slicing. Stop the old job.
  251. self._terminate()
  252. self._createSocket()
  253. if self._process_layers_job is not None:
  254. # We were processing layers. Stop that, the layers are going to change soon.
  255. Logger.log("i", "Aborting process layers job...")
  256. self._process_layers_job.abort()
  257. self._process_layers_job = None
  258. if self._error_message:
  259. self._error_message.hide()
  260. @pyqtSlot()
  261. def forceSlice(self) -> None:
  262. """Manually triggers a reslice"""
  263. self.markSliceAll()
  264. self.slice()
  265. @call_on_qt_thread # Must be called from the main thread because of OpenGL
  266. def _createSnapshot(self) -> None:
  267. self._snapshot = None
  268. if not CuraApplication.getInstance().isVisible:
  269. Logger.log("w", "Can't create snapshot when renderer not initialized.")
  270. return
  271. Logger.log("i", "Creating thumbnail image (just before slice)...")
  272. try:
  273. self._snapshot = Snapshot.snapshot(width = 300, height = 300)
  274. except Exception:
  275. Logger.logException("w", "Failed to create snapshot image")
  276. self._snapshot = None # Failing to create thumbnail should not fail creation of UFP
  277. def getLatestSnapshot(self) -> Optional[QImage]:
  278. return self._snapshot
  279. def slice(self) -> None:
  280. """Perform a slice of the scene."""
  281. if not self._init_done:
  282. self._immediate_slice_after_init = True
  283. return
  284. self._immediate_slice_after_init = False
  285. self._createSnapshot()
  286. self.startPlugins()
  287. Logger.log("i", "Starting to slice...")
  288. self._time_start_process = time()
  289. if not self._build_plates_to_be_sliced:
  290. self.processingProgress.emit(1.0)
  291. Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
  292. self.setState(BackendState.Done)
  293. return
  294. if self._process_layers_job:
  295. Logger.log("d", "Process layers job still busy, trying later.")
  296. return
  297. if not hasattr(self._scene, "gcode_dict"):
  298. self._scene.gcode_dict = {} # type: ignore
  299. # We need to ignore type because we are creating the missing attribute here.
  300. # see if we really have to slice
  301. application = CuraApplication.getInstance()
  302. active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
  303. build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
  304. Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
  305. num_objects = self._numObjectsPerBuildPlate()
  306. self._stored_layer_data = []
  307. if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
  308. self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore
  309. # We need to ignore the type because we created this attribute above.
  310. Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
  311. if self._build_plates_to_be_sliced:
  312. self.slice()
  313. return
  314. self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
  315. if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
  316. application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
  317. if self._process is None: # type: ignore
  318. self._createSocket()
  319. self.stopSlicing()
  320. self._engine_is_fresh = False # Yes we're going to use the engine
  321. self.processingProgress.emit(0.0)
  322. self.backendStateChange.emit(BackendState.NotStarted)
  323. self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore #[] indexed by build plate number
  324. self._slicing = True
  325. self.slicingStarted.emit()
  326. self.determineAutoSlicing() # Switch timer on or off if appropriate
  327. slice_message = self._socket.createMessage("cura.proto.Slice")
  328. self._start_slice_job = StartSliceJob(slice_message)
  329. self._start_slice_job_build_plate = build_plate_to_be_sliced
  330. self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
  331. self._start_slice_job.start()
  332. self._start_slice_job.finished.connect(self._onStartSliceCompleted)
  333. def _terminate(self) -> None:
  334. """Terminate the engine process.
  335. Start the engine process by calling _createSocket()
  336. """
  337. self._slicing = False
  338. self._stored_layer_data = []
  339. if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
  340. del self._stored_optimized_layer_data[self._start_slice_job_build_plate]
  341. if self._start_slice_job is not None:
  342. self._start_slice_job.cancel()
  343. self.stopPlugins()
  344. self.slicingCancelled.emit()
  345. self.processingProgress.emit(0)
  346. Logger.log("d", "Attempting to kill the engine process")
  347. if CuraApplication.getInstance().getUseExternalBackend():
  348. return
  349. if self._process is not None: # type: ignore
  350. Logger.log("d", "Killing engine process")
  351. try:
  352. self._process.terminate() # type: ignore
  353. Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore
  354. self._process = None # type: ignore
  355. except Exception as e:
  356. # Terminating a process that is already terminating causes an exception, silently ignore this.
  357. Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
  358. def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
  359. """Event handler to call when the job to initiate the slicing process is
  360. completed.
  361. When the start slice job is successfully completed, it will be happily
  362. slicing. This function handles any errors that may occur during the
  363. bootstrapping of a slice job.
  364. :param job: The start slice job that was just finished.
  365. """
  366. if self._error_message:
  367. self._error_message.hide()
  368. # Note that cancelled slice jobs can still call this method.
  369. if self._start_slice_job is job:
  370. self._start_slice_job = None
  371. if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error:
  372. self.setState(BackendState.Error)
  373. self.backendError.emit(job)
  374. return
  375. application = CuraApplication.getInstance()
  376. if job.getResult() == StartJobResult.MaterialIncompatible:
  377. if application.platformActivity:
  378. self._error_message = Message(catalog.i18nc("@info:status",
  379. "Unable to slice with the current material as it is incompatible with the selected machine or configuration."),
  380. title = catalog.i18nc("@info:title", "Unable to slice"),
  381. message_type = Message.MessageType.WARNING)
  382. self._error_message.show()
  383. self.setState(BackendState.Error)
  384. self.backendError.emit(job)
  385. else:
  386. self.setState(BackendState.NotStarted)
  387. return
  388. if job.getResult() == StartJobResult.SettingError:
  389. if application.platformActivity:
  390. if not self._global_container_stack:
  391. Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
  392. return
  393. extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
  394. error_keys: List[str] = []
  395. for extruder in extruders:
  396. error_keys.extend(extruder.getErrorKeys())
  397. if not extruders:
  398. error_keys = self._global_container_stack.getErrorKeys()
  399. error_labels = set()
  400. for key in error_keys:
  401. 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.
  402. definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key)
  403. if definitions:
  404. break #Found it! No need to continue search.
  405. else: #No stack has a definition for this setting.
  406. Logger.log("w", "When checking settings for errors, unable to find definition for key: {key}".format(key = key))
  407. continue
  408. error_labels.add(definitions[0].label)
  409. self._error_message = Message(catalog.i18nc("@info:status",
  410. "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)),
  411. title = catalog.i18nc("@info:title", "Unable to slice"),
  412. message_type = Message.MessageType.WARNING)
  413. Logger.warning(f"Unable to slice with the current settings. The following settings have errors: {', '.join(error_labels)}")
  414. self._error_message.show()
  415. self.setState(BackendState.Error)
  416. self.backendError.emit(job)
  417. else:
  418. self.setState(BackendState.NotStarted)
  419. return
  420. elif job.getResult() == StartJobResult.ObjectSettingError:
  421. errors = {}
  422. for node in DepthFirstIterator(application.getController().getScene().getRoot()):
  423. stack = node.callDecoration("getStack")
  424. if not stack:
  425. continue
  426. for key in stack.getErrorKeys():
  427. if not self._global_container_stack:
  428. Logger.log("e", "CuraEngineBackend does not have global_container_stack assigned.")
  429. continue
  430. definition = cast(DefinitionContainerInterface, self._global_container_stack.getBottom()).findDefinitions(key = key)
  431. if not definition:
  432. Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
  433. continue
  434. errors[key] = definition[0].label
  435. self._error_message = Message(catalog.i18nc("@info:status",
  436. "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())),
  437. title = catalog.i18nc("@info:title", "Unable to slice"),
  438. message_type = Message.MessageType.WARNING)
  439. Logger.warning(f"Unable to slice due to per-object settings. The following settings have errors on one or more models: {', '.join(errors.values())}")
  440. self._error_message.show()
  441. self.setState(BackendState.Error)
  442. self.backendError.emit(job)
  443. return
  444. if job.getResult() == StartJobResult.BuildPlateError:
  445. if application.platformActivity:
  446. self._error_message = Message(catalog.i18nc("@info:status",
  447. "Unable to slice because the prime tower or prime position(s) are invalid."),
  448. title = catalog.i18nc("@info:title", "Unable to slice"),
  449. message_type = Message.MessageType.WARNING)
  450. self._error_message.show()
  451. self.setState(BackendState.Error)
  452. self.backendError.emit(job)
  453. return
  454. else:
  455. self.setState(BackendState.NotStarted)
  456. if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
  457. self._error_message = Message(catalog.i18nc("@info:status",
  458. "Unable to slice because there are objects associated with disabled Extruder %s.") % job.getAssociatedDisabledExtruders(),
  459. title = catalog.i18nc("@info:title", "Unable to slice"),
  460. message_type = Message.MessageType.WARNING)
  461. self._error_message.show()
  462. self.setState(BackendState.Error)
  463. self.backendError.emit(job)
  464. return
  465. if job.getResult() == StartJobResult.NothingToSlice:
  466. if application.platformActivity:
  467. self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
  468. "\n- Fit within the build volume"
  469. "\n- Are assigned to an enabled extruder"
  470. "\n- Are not all set as modifier meshes"),
  471. title = catalog.i18nc("@info:title", "Unable to slice"),
  472. message_type = Message.MessageType.WARNING)
  473. self._error_message.show()
  474. self.setState(BackendState.Error)
  475. self.backendError.emit(job)
  476. else:
  477. self.setState(BackendState.NotStarted)
  478. self._invokeSlice()
  479. return
  480. # Preparation completed, send it to the backend.
  481. immediate_success = self._socket.sendMessage(job.getSliceMessage())
  482. if (not CuraApplication.getInstance().getUseExternalBackend()) and (not immediate_success):
  483. if self._last_socket_error is not None and self._last_socket_error.getErrorCode() == Arcus.ErrorCode.MessageTooBigError:
  484. error_txt = catalog.i18nc("@info:status", "Unable to send the model data to the engine. Please try to use a less detailed model, or reduce the number of instances.")
  485. else:
  486. error_txt = catalog.i18nc("@info:status", "Unable to send the model data to the engine. Please try again, or contact support.")
  487. self._error_message = Message(error_txt,
  488. title=catalog.i18nc("@info:title", "Unable to slice"),
  489. message_type=Message.MessageType.WARNING)
  490. self._error_message.show()
  491. self.setState(BackendState.Error)
  492. self.backendError.emit(job)
  493. return
  494. # Notify the user that it's now up to the backend to do its job
  495. self.setState(BackendState.Processing)
  496. # Handle time reporting.
  497. self._time_send_message = time()
  498. if self._time_start_process:
  499. Logger.log("d", "Sending slice message took %s seconds", self._time_send_message - self._time_start_process)
  500. def determineAutoSlicing(self) -> bool:
  501. """Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
  502. It disables when:
  503. - preference auto slice is off
  504. - decorator isBlockSlicing is found (used in g-code reader)
  505. """
  506. enable_timer = True
  507. self._is_disabled = False
  508. if not CuraApplication.getInstance().getPreferences().getValue("general/auto_slice"):
  509. enable_timer = False
  510. for node in DepthFirstIterator(self._scene.getRoot()):
  511. if node.callDecoration("isBlockSlicing"):
  512. enable_timer = False
  513. self.setState(BackendState.Disabled)
  514. self._is_disabled = True
  515. gcode_list = node.callDecoration("getGCodeList")
  516. if gcode_list is not None:
  517. self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list # type: ignore
  518. # We need to ignore type because we generate this attribute dynamically.
  519. if self._use_timer == enable_timer:
  520. return self._use_timer
  521. if enable_timer:
  522. self.setState(BackendState.NotStarted)
  523. self.enableTimer()
  524. return True
  525. else:
  526. self.disableTimer()
  527. return False
  528. def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
  529. """Return a dict with number of objects per build plate"""
  530. num_objects: Dict[int, int] = defaultdict(int)
  531. for node in DepthFirstIterator(self._scene.getRoot()):
  532. # Only count sliceable objects
  533. if node.callDecoration("isSliceable"):
  534. build_plate_number = node.callDecoration("getBuildPlateNumber")
  535. if build_plate_number is not None:
  536. num_objects[build_plate_number] += 1
  537. return num_objects
  538. def _onSceneChanged(self, source: SceneNode) -> None:
  539. """Listener for when the scene has changed.
  540. This should start a slice if the scene is now ready to slice.
  541. :param source: The scene node that was changed.
  542. """
  543. if not source.callDecoration("isSliceable") and source != self._scene.getRoot():
  544. return
  545. # This case checks if the source node is a node that contains GCode. In this case the
  546. # current layer data is removed so the previous data is not rendered - CURA-4821
  547. if source.callDecoration("isBlockSlicing") and source.callDecoration("getLayerData"):
  548. self._stored_optimized_layer_data = {}
  549. build_plate_changed = set()
  550. source_build_plate_number = source.callDecoration("getBuildPlateNumber")
  551. if source == self._scene.getRoot():
  552. # we got the root node
  553. num_objects = self._numObjectsPerBuildPlate()
  554. for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
  555. if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
  556. self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
  557. build_plate_changed.add(build_plate_number)
  558. else:
  559. # we got a single scenenode
  560. if not source.callDecoration("isGroup"):
  561. mesh_data = source.getMeshData()
  562. if mesh_data is None or mesh_data.getVertices() is None:
  563. return
  564. # There are some SceneNodes that do not have any build plate associated, then do not add to the list.
  565. if source_build_plate_number is not None:
  566. build_plate_changed.add(source_build_plate_number)
  567. if not build_plate_changed:
  568. return
  569. if self._tool_active:
  570. # do it later, each source only has to be done once
  571. if source not in self._postponed_scene_change_sources:
  572. self._postponed_scene_change_sources.append(source)
  573. return
  574. self.stopSlicing()
  575. for build_plate_number in build_plate_changed:
  576. if build_plate_number not in self._build_plates_to_be_sliced:
  577. self._build_plates_to_be_sliced.append(build_plate_number)
  578. self.printDurationMessage.emit(source_build_plate_number, {}, [])
  579. self.processingProgress.emit(0.0)
  580. self._clearLayerData(build_plate_changed)
  581. self._invokeSlice()
  582. def _onSocketError(self, error: Arcus.Error) -> None:
  583. """Called when an error occurs in the socket connection towards the engine.
  584. :param error: The exception that occurred.
  585. """
  586. if CuraApplication.getInstance().isShuttingDown():
  587. return
  588. super()._onSocketError(error)
  589. if error.getErrorCode() == Arcus.ErrorCode.Debug:
  590. return
  591. self._last_socket_error = error
  592. self._terminate()
  593. self._createSocket()
  594. if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError,
  595. Arcus.ErrorCode.ConnectionResetError,
  596. Arcus.ErrorCode.Debug]:
  597. Logger.log("w", "A socket error caused the connection to be reset")
  598. # _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status
  599. # needs to be updated. Otherwise, backendState is "Unable To Slice"
  600. if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
  601. self._start_slice_job.setIsCancelled(False)
  602. # Check if there's any slicable object in the scene.
  603. def hasSlicableObject(self) -> bool:
  604. has_slicable = False
  605. for node in DepthFirstIterator(self._scene.getRoot()):
  606. if node.callDecoration("isSliceable"):
  607. has_slicable = True
  608. break
  609. return has_slicable
  610. def _clearLayerData(self, build_plate_numbers: Set = None) -> None:
  611. """Remove old layer data (if any)"""
  612. # Clear out any old gcode
  613. self._scene.gcode_dict = {} # type: ignore
  614. for node in DepthFirstIterator(self._scene.getRoot()):
  615. if node.callDecoration("getLayerData"):
  616. if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
  617. # We can assume that all nodes have a parent as we're looping through the scene and filter out root
  618. cast(SceneNode, node.getParent()).removeChild(node)
  619. def markSliceAll(self) -> None:
  620. for build_plate_number in range(CuraApplication.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
  621. if build_plate_number not in self._build_plates_to_be_sliced:
  622. self._build_plates_to_be_sliced.append(build_plate_number)
  623. def needsSlicing(self) -> None:
  624. """Convenient function: mark everything to slice, emit state and clear layer data"""
  625. # CURA-6604: If there's no slicable object, do not (try to) trigger slice, which will clear all the current
  626. # gcode. This can break Gcode file loading if it tries to remove it afterwards.
  627. if not self.hasSlicableObject():
  628. return
  629. self.determineAutoSlicing()
  630. self.stopSlicing()
  631. self.markSliceAll()
  632. self.processingProgress.emit(0.0)
  633. if not self._use_timer:
  634. # With manually having to slice, we want to clear the old invalid layer data.
  635. self._clearLayerData()
  636. def _onSettingChanged(self, instance: SettingInstance, property: str) -> None:
  637. """A setting has changed, so check if we must reslice.
  638. :param instance: The setting instance that has changed.
  639. :param property: The property of the setting instance that has changed.
  640. """
  641. if property == "value": # Only re-slice if the value has changed.
  642. self.needsSlicing()
  643. self._onChanged()
  644. elif property == "validationState":
  645. if self._use_timer:
  646. self._change_timer.stop()
  647. def _onStackErrorCheckFinished(self) -> None:
  648. self.determineAutoSlicing()
  649. if self._is_disabled:
  650. return
  651. if not self._slicing and self._build_plates_to_be_sliced:
  652. self.needsSlicing()
  653. self._onChanged()
  654. def _onLayerMessage(self, message: Arcus.PythonMessage) -> None:
  655. """Called when a sliced layer data message is received from the engine.
  656. :param message: The protobuf message containing sliced layer data.
  657. """
  658. self._stored_layer_data.append(message)
  659. def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None:
  660. """Called when an optimized sliced layer data message is received from the engine.
  661. :param message: The protobuf message containing sliced layer data.
  662. """
  663. if self._start_slice_job_build_plate is not None:
  664. if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
  665. self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
  666. self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
  667. def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
  668. """Called when a progress message is received from the engine.
  669. :param message: The protobuf message containing the slicing progress.
  670. """
  671. self.processingProgress.emit(message.amount)
  672. self.setState(BackendState.Processing)
  673. def _invokeSlice(self) -> None:
  674. if self._use_timer:
  675. # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
  676. # otherwise business as usual
  677. if self._machine_error_checker is None:
  678. self._change_timer.stop()
  679. return
  680. if self._machine_error_checker.needToWaitForResult:
  681. self._change_timer.stop()
  682. else:
  683. self._change_timer.start()
  684. def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
  685. """Called when the engine sends a message that slicing is finished.
  686. :param message: The protobuf message signalling that slicing is finished.
  687. """
  688. self.stopPlugins()
  689. self.setState(BackendState.Done)
  690. self.processingProgress.emit(1.0)
  691. self._time_end_slice = time()
  692. try:
  693. gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore
  694. # We need to ignore the type because it was generated dynamically.
  695. except KeyError:
  696. # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
  697. gcode_list = []
  698. application = CuraApplication.getInstance()
  699. for index, line in enumerate(gcode_list):
  700. replaced = line.replace("{print_time}", str(application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
  701. replaced = replaced.replace("{filament_amount}", str(application.getPrintInformation().materialLengths))
  702. replaced = replaced.replace("{filament_weight}", str(application.getPrintInformation().materialWeights))
  703. replaced = replaced.replace("{filament_cost}", str(application.getPrintInformation().materialCosts))
  704. replaced = replaced.replace("{jobname}", str(application.getPrintInformation().jobName))
  705. gcode_list[index] = replaced
  706. self._slicing = False
  707. if self._time_start_process:
  708. Logger.log("d", "Slicing took %s seconds", time() - self._time_start_process)
  709. Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate()))
  710. # See if we need to process the sliced layers job.
  711. active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
  712. if (
  713. self._layer_view_active and
  714. (self._process_layers_job is None or not self._process_layers_job.isRunning()) and
  715. active_build_plate == self._start_slice_job_build_plate and
  716. active_build_plate not in self._build_plates_to_be_sliced):
  717. self._startProcessSlicedLayersJob(active_build_plate)
  718. # self._onActiveViewChanged()
  719. self._start_slice_job_build_plate = None
  720. Logger.log("d", "See if there is more to slice...")
  721. # Somehow this results in an Arcus Error
  722. # self.slice()
  723. # Call slice again using the timer, allowing the backend to restart
  724. if self._build_plates_to_be_sliced:
  725. self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode
  726. self._invokeSlice()
  727. def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None:
  728. """Called when a g-code message is received from the engine.
  729. :param message: The protobuf message containing g-code, encoded as UTF-8.
  730. """
  731. try:
  732. 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.
  733. except KeyError:
  734. # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
  735. pass # Throw the message away.
  736. def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
  737. """Called when a g-code prefix message is received from the engine.
  738. :param message: The protobuf message containing the g-code prefix,
  739. encoded as UTF-8.
  740. """
  741. try:
  742. 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.
  743. except KeyError:
  744. # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
  745. pass # Throw the message away.
  746. def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None:
  747. application = CuraApplication.getInstance()
  748. application.getPrintInformation().slice_uuid = message.slice_uuid
  749. def _createSocket(self, protocol_file: str = None) -> None:
  750. """Creates a new socket connection."""
  751. if not protocol_file:
  752. if not self.getPluginId():
  753. Logger.error("Can't create socket before CuraEngineBackend plug-in is registered.")
  754. return
  755. plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
  756. if not plugin_path:
  757. Logger.error("Could not get plugin path!", self.getPluginId())
  758. return
  759. protocol_file = os.path.abspath(os.path.join(plugin_path, "Cura.proto"))
  760. super()._createSocket(protocol_file)
  761. self._engine_is_fresh = True
  762. def _onChanged(self, *args: Any, **kwargs: Any) -> None:
  763. """Called when anything has changed to the stuff that needs to be sliced.
  764. This indicates that we should probably re-slice soon.
  765. """
  766. self.needsSlicing()
  767. if self._use_timer:
  768. # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
  769. # otherwise business as usual
  770. if self._machine_error_checker is None:
  771. self._change_timer.stop()
  772. return
  773. if self._machine_error_checker.needToWaitForResult:
  774. self._change_timer.stop()
  775. else:
  776. self._change_timer.start()
  777. def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None:
  778. """Called when a print time message is received from the engine.
  779. :param message: The protobuf message containing the print time per feature and
  780. material amount per extruder
  781. """
  782. material_amounts = []
  783. for index in range(message.repeatedMessageCount("materialEstimates")):
  784. material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
  785. times = self._parseMessagePrintTimes(message)
  786. self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts)
  787. def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]:
  788. """Called for parsing message to retrieve estimated time per feature
  789. :param message: The protobuf message containing the print time per feature
  790. """
  791. result = {
  792. "inset_0": message.time_inset_0,
  793. "inset_x": message.time_inset_x,
  794. "skin": message.time_skin,
  795. "infill": message.time_infill,
  796. "support_infill": message.time_support_infill,
  797. "support_interface": message.time_support_interface,
  798. "support": message.time_support,
  799. "skirt": message.time_skirt,
  800. "prime_tower": message.time_prime_tower,
  801. "travel": message.time_travel,
  802. "retract": message.time_retract,
  803. "none": message.time_none
  804. }
  805. return result
  806. def _onBackendConnected(self) -> None:
  807. """Called when the back-end connects to the front-end."""
  808. if self._restart:
  809. self._restart = False
  810. self._onChanged()
  811. def _onToolOperationStarted(self, tool: Tool) -> None:
  812. """Called when the user starts using some tool.
  813. When the user starts using a tool, we should pause slicing to prevent
  814. continuously slicing while the user is dragging some tool handle.
  815. :param tool: The tool that the user is using.
  816. """
  817. self._tool_active = True # Do not react on scene change
  818. self.disableTimer()
  819. # Restart engine as soon as possible, we know we want to slice afterwards
  820. if not self._engine_is_fresh:
  821. self._terminate()
  822. self._createSocket()
  823. def _onToolOperationStopped(self, tool: Tool) -> None:
  824. """Called when the user stops using some tool.
  825. This indicates that we can safely start slicing again.
  826. :param tool: The tool that the user was using.
  827. """
  828. self._tool_active = False # React on scene change again
  829. self.determineAutoSlicing() # Switch timer on if appropriate
  830. # Process all the postponed scene changes
  831. while self._postponed_scene_change_sources:
  832. source = self._postponed_scene_change_sources.pop(0)
  833. self._onSceneChanged(source)
  834. def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None:
  835. self._process_layers_job = ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
  836. self._process_layers_job.setBuildPlate(build_plate_number)
  837. self._process_layers_job.finished.connect(self._onProcessLayersFinished)
  838. self._process_layers_job.start()
  839. def _onActiveViewChanged(self) -> None:
  840. """Called when the user changes the active view mode."""
  841. view = CuraApplication.getInstance().getController().getActiveView()
  842. if view:
  843. active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  844. if view.getPluginId() == "SimulationView":
  845. # If switching to layer view, we should process the layers if that hasn't been done yet.
  846. self._layer_view_active = True
  847. # There is data and we're not slicing at the moment
  848. # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
  849. # TODO: what build plate I am slicing
  850. if (active_build_plate in self._stored_optimized_layer_data and
  851. not self._slicing and
  852. not self._process_layers_job and
  853. active_build_plate not in self._build_plates_to_be_sliced):
  854. self._startProcessSlicedLayersJob(active_build_plate)
  855. else:
  856. self._layer_view_active = False
  857. def _onBackendQuit(self) -> None:
  858. """Called when the back-end self-terminates.
  859. We should reset our state and start listening for new connections.
  860. """
  861. if not self._restart:
  862. if self._process: # type: ignore
  863. return_code = self._process.wait()
  864. if return_code != 0:
  865. Logger.log("e", f"Backend exited abnormally with return code {return_code}!")
  866. self._slicing_error_message.show()
  867. self.setState(BackendState.Error)
  868. self.stopSlicing()
  869. else:
  870. Logger.log("d", "Backend finished slicing. Resetting process and socket.")
  871. self.stopPlugins()
  872. self._process = None # type: ignore
  873. def _reportBackendError(self, _message_id: str, _action_id: str) -> None:
  874. """
  875. Triggered when the user wants to report an error in the back-end.
  876. """
  877. QDesktopServices.openUrl(QUrl("https://github.com/Ultimaker/Cura/issues/new/choose"))
  878. def _onGlobalStackChanged(self) -> None:
  879. """Called when the global container stack changes"""
  880. if self._global_container_stack:
  881. self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
  882. self._global_container_stack.containersChanged.disconnect(self._onChanged)
  883. for extruder in self._global_container_stack.extruderList:
  884. extruder.propertyChanged.disconnect(self._onSettingChanged)
  885. extruder.containersChanged.disconnect(self._onChanged)
  886. self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine
  887. if self._global_container_stack:
  888. # Note: Only starts slicing when the value changed.
  889. self._global_container_stack.propertyChanged.connect(self._onSettingChanged)
  890. self._global_container_stack.containersChanged.connect(self._onChanged)
  891. for extruder in self._global_container_stack.extruderList:
  892. extruder.propertyChanged.connect(self._onSettingChanged)
  893. extruder.containersChanged.connect(self._onChanged)
  894. self._onChanged()
  895. def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
  896. if job.getBuildPlate() in self._stored_optimized_layer_data:
  897. del self._stored_optimized_layer_data[job.getBuildPlate()]
  898. else:
  899. Logger.log("w", "The optimized layer data was already deleted for buildplate %s", job.getBuildPlate())
  900. self._process_layers_job = None
  901. Logger.log("d", "See if there is more to slice(2)...")
  902. self._invokeSlice()
  903. def enableTimer(self) -> None:
  904. """Connect slice function to timer."""
  905. if not self._use_timer:
  906. self._change_timer.timeout.connect(self.slice)
  907. self._use_timer = True
  908. def disableTimer(self) -> None:
  909. """Disconnect slice function from timer.
  910. This means that slicing will not be triggered automatically
  911. """
  912. if self._use_timer:
  913. self._use_timer = False
  914. self._change_timer.timeout.disconnect(self.slice)
  915. def _onPreferencesChanged(self, preference: str) -> None:
  916. if preference != "general/auto_slice" and preference != "info/send_engine_crash" and preference != "info/anonymous_engine_crash_report":
  917. return
  918. if preference == "general/auto_slice":
  919. auto_slice = self.determineAutoSlicing()
  920. if auto_slice:
  921. self._change_timer.start()
  922. elif preference == "info/send_engine_crash":
  923. os.environ["USE_SENTRY"] = "1" if CuraApplication.getInstance().getPreferences().getValue("info/send_engine_crash") else "0"
  924. def tickle(self) -> None:
  925. """Tickle the backend so in case of auto slicing, it starts the timer."""
  926. if self._use_timer:
  927. self._change_timer.start()
  928. def _extruderChanged(self) -> None:
  929. if not self._multi_build_plate_model:
  930. Logger.log("w", "CuraEngineBackend does not have multi_build_plate_model assigned!")
  931. return
  932. for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
  933. if build_plate_number not in self._build_plates_to_be_sliced:
  934. self._build_plates_to_be_sliced.append(build_plate_number)
  935. self._invokeSlice()