Merge pull request #3031 from Ultimaker/feature_headless_docker

Feature headless
This commit is contained in:
ChrisTerBeke 2018-01-19 10:31:20 +01:00 committed by GitHub
commit fb8aa08c6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 138 additions and 79 deletions

View file

@ -88,3 +88,5 @@ class ArrangeObjectsJob(Job):
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
no_full_solution_message.show() no_full_solution_message.show()
self.finished.emit(self)

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -25,7 +26,7 @@ catalog = i18nCatalog("cura")
import numpy import numpy
import math import math
from typing import List from typing import List, Optional
# Setting for clearance around the prime # Setting for clearance around the prime
PRIME_CLEARANCE = 6.5 PRIME_CLEARANCE = 6.5
@ -194,8 +195,7 @@ class BuildVolume(SceneNode):
return True return True
## For every sliceable node, update node._outside_buildarea ## For every sliceable node, update outsideBuildArea
#
def updateNodeBoundaryCheck(self): def updateNodeBoundaryCheck(self):
root = Application.getInstance().getController().getScene().getRoot() root = Application.getInstance().getController().getScene().getRoot()
nodes = list(BreadthFirstIterator(root)) nodes = list(BreadthFirstIterator(root))
@ -212,35 +212,51 @@ class BuildVolume(SceneNode):
for node in nodes: for node in nodes:
# Need to check group nodes later # Need to check group nodes later
if node.callDecoration("isGroup"): self.checkBoundsAndUpdate(node, bounds = build_volume_bounding_box)
group_nodes.append(node) # Keep list of affected group_nodes
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
node._outside_buildarea = False
bbox = node.getBoundingBox()
# Mark the node as outside the build volume if the bounding box test fails.
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
node._outside_buildarea = True
continue
convex_hull = node.callDecoration("getConvexHull")
if convex_hull:
if not convex_hull.isValid():
return
# Check for collisions between disallowed areas and the object
for area in self.getDisallowedAreas():
overlap = convex_hull.intersectsPolygon(area)
if overlap is None:
continue
node._outside_buildarea = True
continue
# Group nodes should override the _outside_buildarea property of their children. # Group nodes should override the _outside_buildarea property of their children.
for group_node in group_nodes: for group_node in group_nodes:
for child_node in group_node.getAllChildren(): for child_node in group_node.getAllChildren():
child_node._outside_buildarea = group_node._outside_buildarea child_node._outside_buildarea = group_node._outside_buildarea
## Update the outsideBuildArea of a single node, given bounds or current build volume
def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
if not isinstance(node, CuraSceneNode):
return
if bounds is None:
build_volume_bounding_box = self.getBoundingBox()
if build_volume_bounding_box:
# It's over 9000!
build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
else:
# No bounding box. This is triggered when running Cura from command line with a model for the first time
# In that situation there is a model, but no machine (and therefore no build volume.
return
else:
build_volume_bounding_box = bounds
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
bbox = node.getBoundingBox()
# Mark the node as outside the build volume if the bounding box test fails.
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
node.setOutsideBuildArea(True)
return
convex_hull = self.callDecoration("getConvexHull")
if convex_hull:
if not convex_hull.isValid():
return
# Check for collisions between disallowed areas and the object
for area in self.getDisallowedAreas():
overlap = convex_hull.intersectsPolygon(area)
if overlap is None:
continue
node.setOutsideBuildArea(True)
return
node.setOutsideBuildArea(False)
## Recalculates the build volume & disallowed areas. ## Recalculates the build volume & disallowed areas.
def rebuild(self): def rebuild(self):
if not self._width or not self._height or not self._depth: if not self._width or not self._height or not self._depth:

View file

@ -142,6 +142,7 @@ class CuraApplication(QtApplication):
if not hasattr(sys, "frozen"): if not hasattr(sys, "frozen"):
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")) Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
self._use_gui = True
self._open_file_queue = [] # Files to open when plug-ins are loaded. self._open_file_queue = [] # Files to open when plug-ins are loaded.
# Need to do this before ContainerRegistry tries to load the machines # Need to do this before ContainerRegistry tries to load the machines
@ -452,7 +453,7 @@ class CuraApplication(QtApplication):
elif choice == "always_keep": elif choice == "always_keep":
# don't show dialog and KEEP the profile # don't show dialog and KEEP the profile
self.discardOrKeepProfileChangesClosed("keep") self.discardOrKeepProfileChangesClosed("keep")
else: elif self._use_gui:
# ALWAYS ask whether to keep or discard the profile # ALWAYS ask whether to keep or discard the profile
self.showDiscardOrKeepProfileChanges.emit() self.showDiscardOrKeepProfileChanges.emit()
has_user_interaction = True has_user_interaction = True
@ -652,12 +653,47 @@ class CuraApplication(QtApplication):
def run(self): def run(self):
self.preRun() self.preRun()
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) # Check if we should run as single instance or not
self._setUpSingleInstanceServer() self._setUpSingleInstanceServer()
# Setup scene and build volume
root = self.getController().getScene().getRoot()
self._volume = BuildVolume.BuildVolume(self.getController().getScene().getRoot())
Arrange.build_volume = self._volume
# initialize info objects
self._print_information = PrintInformation.PrintInformation()
self._cura_actions = CuraActions.CuraActions(self)
# Detect in which mode to run and execute that mode
if self.getCommandLineOption("headless", False):
self.runWithoutGUI()
else:
self.runWithGUI()
# Pre-load files if requested
for file_name in self.getCommandLineOption("file", []):
self._openFile(file_name)
for file_name in self._open_file_queue: # Open all the files that were queued up while plug-ins were loading.
self._openFile(file_name)
self._started = True
self.exec_()
## Run Cura without GUI elements and interaction (server mode).
def runWithoutGUI(self):
self._use_gui = False
self.closeSplash()
## Run Cura with GUI (desktop mode).
def runWithGUI(self):
self._use_gui = True
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
controller = self.getController() controller = self.getController()
# Initialize UI state
controller.setActiveStage("PrepareStage") controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView") controller.setActiveView("SolidView")
controller.setCameraTool("CameraTool") controller.setCameraTool("CameraTool")
@ -669,67 +705,44 @@ class CuraApplication(QtApplication):
Selection.selectionChanged.connect(self.onSelectionChanged) Selection.selectionChanged.connect(self.onSelectionChanged)
root = controller.getScene().getRoot() # Set default background color for scene
# The platform is a child of BuildVolume
self._volume = BuildVolume.BuildVolume(root)
# Set the build volume of the arranger to the used build volume
Arrange.build_volume = self._volume
self.getRenderer().setBackgroundColor(QColor(245, 245, 245)) self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
# Initialize platform physics
self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume) self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume)
# Initialize camera
root = controller.getScene().getRoot()
camera = Camera("3d", root) camera = Camera("3d", root)
camera.setPosition(Vector(-80, 250, 700)) camera.setPosition(Vector(-80, 250, 700))
camera.setPerspective(True) camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0)) camera.lookAt(Vector(0, 0, 0))
controller.getScene().setActiveCamera("3d") controller.getScene().setActiveCamera("3d")
camera_tool = self.getController().getTool("CameraTool") # Initialize camera tool
camera_tool = controller.getTool("CameraTool")
camera_tool.setOrigin(Vector(0, 100, 0)) camera_tool.setOrigin(Vector(0, 100, 0))
camera_tool.setZoomRange(0.1, 200000) camera_tool.setZoomRange(0.1, 200000)
# Initialize camera animations
self._camera_animation = CameraAnimation.CameraAnimation() self._camera_animation = CameraAnimation.CameraAnimation()
self._camera_animation.setCameraTool(self.getController().getTool("CameraTool")) self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface...")) self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager) # Initialize QML engine
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager",
self.getSettingInheritanceManager)
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager",
self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel)
qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel)
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml")) self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles)) self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
self.initializeEngine()
run_without_gui = self.getCommandLineOption("headless", False) # Make sure the correct stage is activated after QML is loaded
if not run_without_gui: controller.setActiveStage("PrepareStage")
self.initializeEngine()
controller.setActiveStage("PrepareStage")
if run_without_gui or self._engine.rootObjects: # Hide the splash screen
self.closeSplash() self.closeSplash()
for file_name in self.getCommandLineOption("file", []): def hasGui(self):
self._openFile(file_name) return self._use_gui
for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
self._openFile(file_name)
self._started = True
self.exec_()
def getMachineManager(self, *args) -> MachineManager: def getMachineManager(self, *args) -> MachineManager:
if self._machine_manager is None: if self._machine_manager is None:
@ -797,15 +810,25 @@ class CuraApplication(QtApplication):
# \param engine The QML engine. # \param engine The QML engine.
def registerObjects(self, engine): def registerObjects(self, engine):
super().registerObjects(engine) super().registerObjects(engine)
# global contexts
engine.rootContext().setContextProperty("Printer", self) engine.rootContext().setContextProperty("Printer", self)
engine.rootContext().setContextProperty("CuraApplication", self) engine.rootContext().setContextProperty("CuraApplication", self)
self._print_information = PrintInformation.PrintInformation()
engine.rootContext().setContextProperty("PrintInformation", self._print_information) engine.rootContext().setContextProperty("PrintInformation", self._print_information)
self._cura_actions = CuraActions.CuraActions(self)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions) engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController)
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel)
qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel)
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer") qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel") qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
@ -1342,6 +1365,7 @@ class CuraApplication(QtApplication):
pass pass
fileLoaded = pyqtSignal(str) fileLoaded = pyqtSignal(str)
fileCompleted = pyqtSignal(str)
def _reloadMeshFinished(self, job): def _reloadMeshFinished(self, job):
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh! # TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
@ -1459,6 +1483,7 @@ class CuraApplication(QtApplication):
node.setSelectable(True) node.setSelectable(True)
node.setName(os.path.basename(filename)) node.setName(os.path.basename(filename))
self.getBuildVolume().checkBoundsAndUpdate(node)
extension = os.path.splitext(filename)[1] extension = os.path.splitext(filename)[1]
if extension.lower() in self._non_sliceable_extensions: if extension.lower() in self._non_sliceable_extensions:
@ -1495,8 +1520,8 @@ class CuraApplication(QtApplication):
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
# This node is deepcopied from some other node which already has a BuildPlateDecorator, but the deepcopy # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
# of BuildPlateDecorator produces one that's assoicated with build plate -1. So, here we need to check if # of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if
# the BuildPlateDecorator exists or not and always set the correct build plate number. # the BuildPlateDecorator exists or not and always set the correct build plate number.
build_plate_decorator = node.getDecorator(BuildPlateDecorator) build_plate_decorator = node.getDecorator(BuildPlateDecorator)
if build_plate_decorator is None: if build_plate_decorator is None:
@ -1508,6 +1533,8 @@ class CuraApplication(QtApplication):
op.push() op.push()
scene.sceneChanged.emit(node) scene.sceneChanged.emit(node)
self.fileCompleted.emit(filename)
def addNonSliceableExtension(self, extension): def addNonSliceableExtension(self, extension):
self._non_sliceable_extensions.append(extension) self._non_sliceable_extensions.append(extension)

View file

@ -24,7 +24,10 @@ class ConvexHullNode(SceneNode):
self._original_parent = parent self._original_parent = parent
# Color of the drawn convex hull # Color of the drawn convex hull
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb()) if Application.getInstance().hasGui():
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
else:
self._color = Color(0, 0, 0)
# The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting. # The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
self._mesh_height = 0.1 self._mesh_height = 0.1

View file

@ -1,5 +1,6 @@
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from copy import deepcopy from copy import deepcopy
@ -9,7 +10,7 @@ from copy import deepcopy
class CuraSceneNode(SceneNode): class CuraSceneNode(SceneNode):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._outside_buildarea = True self._outside_buildarea = False
def setOutsideBuildArea(self, new_value): def setOutsideBuildArea(self, new_value):
self._outside_buildarea = new_value self._outside_buildarea = new_value

View file

@ -764,7 +764,7 @@ class MachineManager(QObject):
## Set the active material by switching out a container ## Set the active material by switching out a container
# Depending on from/to material+current variant, a quality profile is chosen and set. # Depending on from/to material+current variant, a quality profile is chosen and set.
@pyqtSlot(str) @pyqtSlot(str)
def setActiveMaterial(self, material_id: str): def setActiveMaterial(self, material_id: str, always_discard_changes = False):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
containers = ContainerRegistry.getInstance().findInstanceContainers(id = material_id) containers = ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
if not containers or not self._active_container_stack: if not containers or not self._active_container_stack:
@ -846,10 +846,10 @@ class MachineManager(QObject):
if not old_quality_changes: if not old_quality_changes:
new_quality_id = candidate_quality.getId() new_quality_id = candidate_quality.getId()
self.setActiveQuality(new_quality_id) self.setActiveQuality(new_quality_id, always_discard_changes = always_discard_changes)
@pyqtSlot(str) @pyqtSlot(str)
def setActiveVariant(self, variant_id: str): def setActiveVariant(self, variant_id: str, always_discard_changes = False):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant_id) containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant_id)
if not containers or not self._active_container_stack: if not containers or not self._active_container_stack:
@ -865,7 +865,7 @@ class MachineManager(QObject):
if old_material: if old_material:
preferred_material_name = old_material.getName() preferred_material_name = old_material.getName()
preferred_material_id = self._updateMaterialContainer(self._global_container_stack.definition, self._global_container_stack, containers[0], preferred_material_name).id preferred_material_id = self._updateMaterialContainer(self._global_container_stack.definition, self._global_container_stack, containers[0], preferred_material_name).id
self.setActiveMaterial(preferred_material_id) self.setActiveMaterial(preferred_material_id, always_discard_changes = always_discard_changes)
else: else:
Logger.log("w", "While trying to set the active variant, no variant was found to replace.") Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
@ -890,10 +890,12 @@ class MachineManager(QObject):
## set the active quality ## set the active quality
# \param quality_id The quality_id of either a quality or a quality_changes # \param quality_id The quality_id of either a quality or a quality_changes
@pyqtSlot(str) @pyqtSlot(str)
def setActiveQuality(self, quality_id: str): def setActiveQuality(self, quality_id: str, always_discard_changes = False):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.blurSettings.emit() self.blurSettings.emit()
Logger.log("d", "Attempting to change the active quality to %s", quality_id)
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = quality_id) containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = quality_id)
if not containers or not self._global_container_stack: if not containers or not self._global_container_stack:
return return
@ -948,11 +950,13 @@ class MachineManager(QObject):
"quality_changes": stack_quality_changes "quality_changes": stack_quality_changes
}) })
Logger.log("d", "Active quality changed")
# show the keep/discard dialog after the containers have been switched. Otherwise, the default values on # show the keep/discard dialog after the containers have been switched. Otherwise, the default values on
# the dialog will be the those before the switching. # the dialog will be the those before the switching.
self._executeDelayedActiveContainerStackChanges() self._executeDelayedActiveContainerStackChanges()
if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1 and not always_discard_changes:
Application.getInstance().discardOrKeepProfileChanges() Application.getInstance().discardOrKeepProfileChanges()
## Used to update material and variant in the active container stack with a delay. ## Used to update material and variant in the active container stack with a delay.
@ -960,6 +964,9 @@ class MachineManager(QObject):
# before the user decided to keep or discard any of their changes using the dialog. # before the user decided to keep or discard any of their changes using the dialog.
# The Application.onDiscardOrKeepProfileChangesClosed signal triggers this method. # The Application.onDiscardOrKeepProfileChangesClosed signal triggers this method.
def _executeDelayedActiveContainerStackChanges(self): def _executeDelayedActiveContainerStackChanges(self):
Logger.log("d", "Applying configuration changes...")
if self._new_variant_container is not None: if self._new_variant_container is not None:
self._active_container_stack.variant = self._new_variant_container self._active_container_stack.variant = self._new_variant_container
self._new_variant_container = None self._new_variant_container = None
@ -984,6 +991,8 @@ class MachineManager(QObject):
self._new_quality_containers.clear() self._new_quality_containers.clear()
Logger.log("d", "New configuration applied")
## Cancel set changes for material and variant in the active container stack. ## Cancel set changes for material and variant in the active container stack.
# Used for ignoring any changes when switching between printers (setActiveMachine) # Used for ignoring any changes when switching between printers (setActiveMachine)
def _cancelDelayedActiveContainerStackChanges(self): def _cancelDelayedActiveContainerStackChanges(self):

View file

@ -291,6 +291,7 @@ class CuraEngineBackend(QObject, Backend):
self._start_slice_job = None self._start_slice_job = None
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error)
return return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: