CuraEngineBackend.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from UM.Backend.Backend import Backend, BackendState
  4. from UM.Application import Application
  5. from UM.Scene.SceneNode import SceneNode
  6. from UM.Preferences import Preferences
  7. from UM.Signal import Signal
  8. from UM.Logger import Logger
  9. from UM.Message import Message
  10. from UM.PluginRegistry import PluginRegistry
  11. from UM.Resources import Resources
  12. from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
  13. from UM.Platform import Platform
  14. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  15. import cura.Settings
  16. from cura.OneAtATimeIterator import OneAtATimeIterator
  17. from cura.Settings.ExtruderManager import ExtruderManager
  18. from . import ProcessSlicedLayersJob
  19. from . import ProcessGCodeJob
  20. from . import StartSliceJob
  21. import os
  22. import sys
  23. from time import time
  24. from PyQt5.QtCore import QTimer
  25. import Arcus
  26. from UM.i18n import i18nCatalog
  27. catalog = i18nCatalog("cura")
  28. class CuraEngineBackend(Backend):
  29. ## Starts the back-end plug-in.
  30. #
  31. # This registers all the signal listeners and prepares for communication
  32. # with the back-end in general.
  33. def __init__(self):
  34. super().__init__()
  35. # Find out where the engine is located, and how it is called.
  36. # This depends on how Cura is packaged and which OS we are running on.
  37. executable_name = "CuraEngine"
  38. if Platform.isWindows():
  39. executable_name += ".exe"
  40. default_engine_location = executable_name
  41. if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)):
  42. default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name)
  43. if hasattr(sys, "frozen"):
  44. default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
  45. if Platform.isLinux() and not default_engine_location:
  46. if not os.getenv("PATH"):
  47. raise OSError("There is something wrong with your Linux installation.")
  48. for pathdir in os.getenv("PATH").split(os.pathsep):
  49. execpath = os.path.join(pathdir, executable_name)
  50. if os.path.exists(execpath):
  51. default_engine_location = execpath
  52. break
  53. if not default_engine_location:
  54. raise EnvironmentError("Could not find CuraEngine")
  55. Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
  56. default_engine_location = os.path.abspath(default_engine_location)
  57. Preferences.getInstance().addPreference("backend/location", default_engine_location)
  58. # Workaround to disable layer view processing if layer view is not active.
  59. self._layer_view_active = False
  60. Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
  61. self._onActiveViewChanged()
  62. self._stored_layer_data = []
  63. self._stored_optimized_layer_data = []
  64. self._scene = Application.getInstance().getController().getScene()
  65. self._scene.sceneChanged.connect(self._onSceneChanged)
  66. # Triggers for when to (re)start slicing:
  67. self._global_container_stack = None
  68. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
  69. self._onGlobalStackChanged()
  70. self._active_extruder_stack = None
  71. cura.Settings.ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
  72. self._onActiveExtruderChanged()
  73. # Listeners for receiving messages from the back-end.
  74. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
  75. self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage
  76. self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
  77. self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
  78. self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
  79. self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
  80. self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
  81. self._start_slice_job = None
  82. self._slicing = False # Are we currently slicing?
  83. self._restart = False # Back-end is currently restarting?
  84. self._enabled = True # Should we be slicing? Slicing might be paused when, for instance, the user is dragging the mesh around.
  85. self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
  86. self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
  87. self._need_slicing = False
  88. self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
  89. self._error_message = None # Pop-up message that shows errors.
  90. self.backendQuit.connect(self._onBackendQuit)
  91. self.backendConnected.connect(self._onBackendConnected)
  92. # When a tool operation is in progress, don't slice. So we need to listen for tool operations.
  93. Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
  94. Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
  95. self._slice_start_time = None
  96. Preferences.getInstance().addPreference("general/auto_slice", True)
  97. self._use_timer = False
  98. self._change_timer = None
  99. self.determineAutoSlicing()
  100. Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
  101. ## Terminate the engine process.
  102. #
  103. # This function should terminate the engine process.
  104. # Called when closing the application.
  105. def close(self):
  106. # Terminate CuraEngine if it is still running at this point
  107. self._terminate()
  108. ## Get the command that is used to call the engine.
  109. # This is useful for debugging and used to actually start the engine.
  110. # \return list of commands and args / parameters.
  111. def getEngineCommand(self):
  112. json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
  113. return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
  114. ## Emitted when we get a message containing print duration and material amount.
  115. # This also implies the slicing has finished.
  116. # \param time The amount of time the print will take.
  117. # \param material_amount The amount of material the print will use.
  118. printDurationMessage = Signal()
  119. ## Emitted when the slicing process starts.
  120. slicingStarted = Signal()
  121. ## Emitted when the slicing process is aborted forcefully.
  122. slicingCancelled = Signal()
  123. def stopSlicing(self):
  124. self.backendStateChange.emit(BackendState.NotStarted)
  125. if self._slicing: # We were already slicing. Stop the old job.
  126. self._terminate()
  127. if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
  128. self._process_layers_job.abort()
  129. self._process_layers_job = None
  130. if self._error_message:
  131. self._error_message.hide()
  132. ## Perform a slice of the scene.
  133. def slice(self):
  134. Logger.log("d", "Starting slice job...")
  135. self._slice_start_time = time()
  136. # if not self._enabled or not self._global_container_stack: # We shouldn't be slicing.
  137. # if self._use_timer:
  138. # # try again in a short time
  139. # self._change_timer.start()
  140. # return
  141. if not self._need_slicing:
  142. Logger.log("d", "Do not need to slice")
  143. return
  144. self.printDurationMessage.emit(0, [0])
  145. self._stored_layer_data = []
  146. self._stored_optimized_layer_data = []
  147. if self._process is None:
  148. Logger.log("d", "Creating socket and start the engine...")
  149. self._createSocket()
  150. self.stopSlicing()
  151. self.processingProgress.emit(0.0)
  152. self.backendStateChange.emit(BackendState.NotStarted)
  153. self._scene.gcode_list = []
  154. self._slicing = True
  155. self.slicingStarted.emit()
  156. slice_message = self._socket.createMessage("cura.proto.Slice")
  157. Logger.log("d", "Really starting slice job")
  158. self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
  159. self._start_slice_job.start()
  160. self._start_slice_job.finished.connect(self._onStartSliceCompleted)
  161. ## Terminate the engine process.
  162. def _terminate(self):
  163. # # Process is none, Try and re-create the socket????
  164. # self._createSocket()
  165. # return
  166. self._slicing = False
  167. self._restart = True
  168. self._stored_layer_data = []
  169. self._stored_optimized_layer_data = []
  170. if self._start_slice_job is not None:
  171. self._start_slice_job.cancel()
  172. self.slicingCancelled.emit()
  173. self.processingProgress.emit(0)
  174. Logger.log("d", "Attempting to kill the engine process")
  175. if Application.getInstance().getCommandLineOption("external-backend", False):
  176. return
  177. if self._process is not None:
  178. Logger.log("d", "Killing engine process")
  179. try:
  180. self._process.terminate()
  181. Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
  182. self._process = None
  183. except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
  184. Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
  185. # else:
  186. # # Process is none, but something did went wrong here. Try and re-create the socket
  187. # self._createSocket()
  188. ## Event handler to call when the job to initiate the slicing process is
  189. # completed.
  190. #
  191. # When the start slice job is successfully completed, it will be happily
  192. # slicing. This function handles any errors that may occur during the
  193. # bootstrapping of a slice job.
  194. #
  195. # \param job The start slice job that was just finished.
  196. def _onStartSliceCompleted(self, job):
  197. if self._error_message:
  198. self._error_message.hide()
  199. # Note that cancelled slice jobs can still call this method.
  200. if self._start_slice_job is job:
  201. self._start_slice_job = None
  202. if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
  203. return
  204. if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
  205. if Application.getInstance().getPlatformActivity:
  206. self._error_message = Message(catalog.i18nc("@info:status",
  207. "The selected material is incompatible with the selected machine or configuration."))
  208. self._error_message.show()
  209. self.backendStateChange.emit(BackendState.Error)
  210. else:
  211. self.backendStateChange.emit(BackendState.NotStarted)
  212. return
  213. if job.getResult() == StartSliceJob.StartJobResult.SettingError:
  214. if Application.getInstance().getPlatformActivity:
  215. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  216. error_keys = []
  217. for extruder in extruders:
  218. error_keys.extend(extruder.getErrorKeys())
  219. if not extruders:
  220. error_keys = self._global_container_stack.getErrorKeys()
  221. error_labels = set()
  222. definition_container = self._global_container_stack.getBottom()
  223. for key in error_keys:
  224. error_labels.add(definition_container.findDefinitions(key = key)[0].label)
  225. error_labels = ", ".join(error_labels)
  226. self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}".format(error_labels)))
  227. self._error_message.show()
  228. self.backendStateChange.emit(BackendState.Error)
  229. else:
  230. self.backendStateChange.emit(BackendState.NotStarted)
  231. return
  232. if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
  233. if Application.getInstance().getPlatformActivity:
  234. self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."))
  235. self._error_message.show()
  236. self.backendStateChange.emit(BackendState.Error)
  237. else:
  238. self.backendStateChange.emit(BackendState.NotStarted)
  239. if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
  240. if Application.getInstance().getPlatformActivity:
  241. 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."))
  242. self._error_message.show()
  243. self.backendStateChange.emit(BackendState.Error)
  244. else:
  245. self.backendStateChange.emit(BackendState.NotStarted)
  246. return
  247. # Preparation completed, send it to the backend.
  248. self._socket.sendMessage(job.getSliceMessage())
  249. # Notify the user that it's now up to the backend to do it's job
  250. self.backendStateChange.emit(BackendState.Processing)
  251. Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
  252. def forceSlice(self):
  253. if self._use_timer:
  254. self._change_timer.start()
  255. else:
  256. self.slice()
  257. ## Determine enable or disable slicing.
  258. # It disables when
  259. # - preference auto slice is off
  260. # - decorator isBlockSlicing is found (used in g-code reader)
  261. def determineAutoSlicing(self):
  262. enable_timer = True
  263. if not Preferences.getInstance().getValue("general/auto_slice"):
  264. enable_timer = False
  265. for node in DepthFirstIterator(self._scene.getRoot()):
  266. if node.callDecoration("isBlockSlicing"):
  267. enable_timer = False
  268. self.backendStateChange.emit(BackendState.Disabled)
  269. gcode_list = node.callDecoration("getGCodeList")
  270. if gcode_list is not None:
  271. self._scene.gcode_list = gcode_list
  272. if self._enabled == enable_timer:
  273. return
  274. if enable_timer:
  275. self._enabled = True
  276. self.backendStateChange.emit(BackendState.NotStarted)
  277. self.enableTimer()
  278. else:
  279. self._enabled = False
  280. # self.close()
  281. self.disableTimer()
  282. ## Listener for when the scene has changed.
  283. #
  284. # This should start a slice if the scene is now ready to slice.
  285. #
  286. # \param source The scene node that was changed.
  287. def _onSceneChanged(self, source):
  288. if type(source) is not SceneNode:
  289. return
  290. if source is self._scene.getRoot():
  291. return
  292. self.determineAutoSlicing()
  293. if source.getMeshData() is None:
  294. return
  295. if source.getMeshData().getVertices() is None:
  296. return
  297. self.needSlicing()
  298. self.stopSlicing()
  299. self._onChanged()
  300. ## Called when an error occurs in the socket connection towards the engine.
  301. #
  302. # \param error The exception that occurred.
  303. def _onSocketError(self, error):
  304. if Application.getInstance().isShuttingDown():
  305. return
  306. super()._onSocketError(error)
  307. if error.getErrorCode() == Arcus.ErrorCode.Debug:
  308. return
  309. self._terminate()
  310. if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
  311. Logger.log("w", "A socket error caused the connection to be reset")
  312. ## TODO: now copied from ProcessSlicedLayersJob. Find my a home.
  313. def _clearLayerData(self):
  314. ## Remove old layer data (if any)
  315. for node in DepthFirstIterator(self._scene.getRoot()):
  316. if node.callDecoration("getLayerData"):
  317. node.getParent().removeChild(node)
  318. break
  319. # if self._abort_requested:
  320. # if self._progress:
  321. # self._progress.hide()
  322. # return
  323. ## Convenient function: set need_slicing, emit state and clear layer data
  324. def needSlicing(self):
  325. self._need_slicing = True
  326. self.processingProgress.emit(0.0)
  327. self.backendStateChange.emit(BackendState.NotStarted)
  328. self._clearLayerData()
  329. ## A setting has changed, so check if we must reslice.
  330. #
  331. # \param instance The setting instance that has changed.
  332. # \param property The property of the setting instance that has changed.
  333. def _onSettingChanged(self, instance, property):
  334. if property == "value": # Only reslice if the value has changed.
  335. self.needSlicing()
  336. self._onChanged()
  337. ## Called when a sliced layer data message is received from the engine.
  338. #
  339. # \param message The protobuf message containing sliced layer data.
  340. def _onLayerMessage(self, message):
  341. self._stored_layer_data.append(message)
  342. ## Called when an optimized sliced layer data message is received from the engine.
  343. #
  344. # \param message The protobuf message containing sliced layer data.
  345. def _onOptimizedLayerMessage(self, message):
  346. self._stored_optimized_layer_data.append(message)
  347. ## Called when a progress message is received from the engine.
  348. #
  349. # \param message The protobuf message containing the slicing progress.
  350. def _onProgressMessage(self, message):
  351. self.processingProgress.emit(message.amount)
  352. self.backendStateChange.emit(BackendState.Processing)
  353. ## Called when the engine sends a message that slicing is finished.
  354. #
  355. # \param message The protobuf message signalling that slicing is finished.
  356. def _onSlicingFinishedMessage(self, message):
  357. self.backendStateChange.emit(BackendState.Done)
  358. self.processingProgress.emit(1.0)
  359. self._slicing = False
  360. self._need_slicing = False
  361. Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
  362. if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
  363. self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
  364. self._process_layers_job.finished.connect(self._onProcessLayersFinished)
  365. self._process_layers_job.start()
  366. self._stored_optimized_layer_data = []
  367. ## Called when a g-code message is received from the engine.
  368. #
  369. # \param message The protobuf message containing g-code, encoded as UTF-8.
  370. def _onGCodeLayerMessage(self, message):
  371. self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))
  372. ## Called when a g-code prefix message is received from the engine.
  373. #
  374. # \param message The protobuf message containing the g-code prefix,
  375. # encoded as UTF-8.
  376. def _onGCodePrefixMessage(self, message):
  377. self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
  378. ## Called when a print time message is received from the engine.
  379. #
  380. # \param message The protobuff message containing the print time and
  381. # material amount per extruder
  382. def _onPrintTimeMaterialEstimates(self, message):
  383. material_amounts = []
  384. for index in range(message.repeatedMessageCount("materialEstimates")):
  385. material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
  386. self.printDurationMessage.emit(message.time, material_amounts)
  387. ## Creates a new socket connection.
  388. def _createSocket(self):
  389. super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
  390. ## Manually triggers a reslice
  391. def forceSlice(self):
  392. if self._use_timer:
  393. self._change_timer.start()
  394. else:
  395. self.slice()
  396. ## Called when anything has changed to the stuff that needs to be sliced.
  397. #
  398. # This indicates that we should probably re-slice soon.
  399. def _onChanged(self, *args, **kwargs):
  400. if self._use_timer:
  401. self.forceSlice()
  402. ## Called when the back-end connects to the front-end.
  403. def _onBackendConnected(self):
  404. if self._restart:
  405. self._onChanged()
  406. self._restart = False
  407. ## Called when the user starts using some tool.
  408. #
  409. # When the user starts using a tool, we should pause slicing to prevent
  410. # continuously slicing while the user is dragging some tool handle.
  411. #
  412. # \param tool The tool that the user is using.
  413. def _onToolOperationStarted(self, tool):
  414. self._enabled = False # Do not reslice when a tool is doing it's 'thing'
  415. if self._use_timer:
  416. self._terminate() # Do not continue slicing once a tool has started
  417. ## Called when the user stops using some tool.
  418. #
  419. # This indicates that we can safely start slicing again.
  420. #
  421. # \param tool The tool that the user was using.
  422. def _onToolOperationStopped(self, tool):
  423. self.needSlicing()
  424. self._enabled = True # Tool stop, start listening for changes again.
  425. ## Called when the user changes the active view mode.
  426. def _onActiveViewChanged(self):
  427. if Application.getInstance().getController().getActiveView():
  428. view = Application.getInstance().getController().getActiveView()
  429. if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet.
  430. self._layer_view_active = True
  431. # There is data and we're not slicing at the moment
  432. # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
  433. if self._stored_optimized_layer_data and not self._slicing:
  434. self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
  435. self._process_layers_job.finished.connect(self._onProcessLayersFinished)
  436. self._process_layers_job.start()
  437. self._stored_optimized_layer_data = []
  438. else:
  439. self._layer_view_active = False
  440. ## Called when the back-end self-terminates.
  441. #
  442. # We should reset our state and start listening for new connections.
  443. def _onBackendQuit(self):
  444. if not self._restart:
  445. if self._process:
  446. Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
  447. self._process = None
  448. ## Called when the global container stack changes
  449. def _onGlobalStackChanged(self):
  450. self.needSlicing()
  451. if self._global_container_stack:
  452. self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
  453. self._global_container_stack.containersChanged.disconnect(self._onChanged)
  454. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  455. if extruders:
  456. for extruder in extruders:
  457. extruder.propertyChanged.disconnect(self._onSettingChanged)
  458. self._global_container_stack = Application.getInstance().getGlobalContainerStack()
  459. if self._global_container_stack:
  460. self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
  461. self._global_container_stack.containersChanged.connect(self._onChanged)
  462. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  463. if extruders:
  464. for extruder in extruders:
  465. extruder.propertyChanged.connect(self._onSettingChanged)
  466. self._onActiveExtruderChanged()
  467. self._onChanged()
  468. def _onActiveExtruderChanged(self):
  469. self.needSlicing()
  470. if self._global_container_stack:
  471. # Connect all extruders of the active machine. This might cause a few connects that have already happend,
  472. # but that shouldn't cause issues as only new / unique connections are added.
  473. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  474. if extruders:
  475. for extruder in extruders:
  476. extruder.propertyChanged.connect(self._onSettingChanged)
  477. if self._active_extruder_stack:
  478. self._active_extruder_stack.containersChanged.disconnect(self._onChanged)
  479. self._active_extruder_stack = cura.Settings.ExtruderManager.getInstance().getActiveExtruderStack()
  480. if self._active_extruder_stack:
  481. self._active_extruder_stack.containersChanged.connect(self._onChanged)
  482. def _onProcessLayersFinished(self, job):
  483. self._process_layers_job = None
  484. def enableTimer(self):
  485. self._enabled = True
  486. self._use_timer = True
  487. self._restart = True
  488. # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
  489. # This timer will group them up, and only slice for the last setting changed signal.
  490. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
  491. self._change_timer = QTimer()
  492. self._change_timer.setInterval(500)
  493. self._change_timer.setSingleShot(True)
  494. self._change_timer.timeout.connect(self.slice)
  495. ## Disable timer.
  496. # This means that slicing will not be triggered automatically
  497. def disableTimer(self):
  498. self._enabled = False
  499. if self._change_timer is not None:
  500. self._change_timer.timeout.disconnect()
  501. self._change_timer = None
  502. self._use_timer = False
  503. self._restart = False
  504. def _onPreferencesChanged(self, preference):
  505. if preference != "general/auto_slice":
  506. return
  507. self.determineAutoSlicing()