diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index f529543779..765c3333cb 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -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"), title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show() + + self.finished.emit(self) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 2567641cc9..7a495b2064 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -1,6 +1,7 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Settings.ExtruderManager import ExtruderManager from UM.Settings.ContainerRegistry import ContainerRegistry from UM.i18n import i18nCatalog @@ -25,7 +26,7 @@ catalog = i18nCatalog("cura") import numpy import math -from typing import List +from typing import List, Optional # Setting for clearance around the prime PRIME_CLEARANCE = 6.5 @@ -194,8 +195,7 @@ class BuildVolume(SceneNode): return True - ## For every sliceable node, update node._outside_buildarea - # + ## For every sliceable node, update outsideBuildArea def updateNodeBoundaryCheck(self): root = Application.getInstance().getController().getScene().getRoot() nodes = list(BreadthFirstIterator(root)) @@ -212,35 +212,51 @@ class BuildVolume(SceneNode): for node in nodes: # Need to check group nodes later - if node.callDecoration("isGroup"): - 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 + self.checkBoundsAndUpdate(node, bounds = build_volume_bounding_box) # Group nodes should override the _outside_buildarea property of their children. for group_node in group_nodes: for child_node in group_node.getAllChildren(): 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. def rebuild(self): if not self._width or not self._height or not self._depth: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 0ac50c9e5e..b825e456c2 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -142,6 +142,7 @@ class CuraApplication(QtApplication): if not hasattr(sys, "frozen"): 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. # Need to do this before ContainerRegistry tries to load the machines @@ -452,7 +453,7 @@ class CuraApplication(QtApplication): elif choice == "always_keep": # don't show dialog and KEEP the profile self.discardOrKeepProfileChangesClosed("keep") - else: + elif self._use_gui: # ALWAYS ask whether to keep or discard the profile self.showDiscardOrKeepProfileChanges.emit() has_user_interaction = True @@ -652,12 +653,47 @@ class CuraApplication(QtApplication): def run(self): 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() + # 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() + # Initialize UI state controller.setActiveStage("PrepareStage") controller.setActiveView("SolidView") controller.setCameraTool("CameraTool") @@ -669,67 +705,44 @@ class CuraApplication(QtApplication): Selection.selectionChanged.connect(self.onSelectionChanged) - root = controller.getScene().getRoot() - - # 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 - + # Set default background color for scene self.getRenderer().setBackgroundColor(QColor(245, 245, 245)) + # Initialize platform physics self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume) + # Initialize camera + root = controller.getScene().getRoot() camera = Camera("3d", root) camera.setPosition(Vector(-80, 250, 700)) camera.setPerspective(True) camera.lookAt(Vector(0, 0, 0)) 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.setZoomRange(0.1, 200000) + # Initialize camera animations self._camera_animation = CameraAnimation.CameraAnimation() self._camera_animation.setCameraTool(self.getController().getTool("CameraTool")) self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface...")) - 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(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) - + # Initialize QML engine self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml")) self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles)) + self.initializeEngine() - run_without_gui = self.getCommandLineOption("headless", False) - if not run_without_gui: - self.initializeEngine() - controller.setActiveStage("PrepareStage") + # Make sure the correct stage is activated after QML is loaded + controller.setActiveStage("PrepareStage") - if run_without_gui or self._engine.rootObjects: - self.closeSplash() + # Hide the splash screen + self.closeSplash() - 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_() + def hasGui(self): + return self._use_gui def getMachineManager(self, *args) -> MachineManager: if self._machine_manager is None: @@ -797,15 +810,25 @@ class CuraApplication(QtApplication): # \param engine The QML engine. def registerObjects(self, engine): super().registerObjects(engine) + + # global contexts engine.rootContext().setContextProperty("Printer", self) engine.rootContext().setContextProperty("CuraApplication", self) - self._print_information = PrintInformation.PrintInformation() engine.rootContext().setContextProperty("PrintInformation", self._print_information) - self._cura_actions = CuraActions.CuraActions(self) engine.rootContext().setContextProperty("CuraActions", self._cura_actions) 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(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel") @@ -1342,6 +1365,7 @@ class CuraApplication(QtApplication): pass fileLoaded = pyqtSignal(str) + fileCompleted = pyqtSignal(str) def _reloadMeshFinished(self, job): # 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.setName(os.path.basename(filename)) + self.getBuildVolume().checkBoundsAndUpdate(node) extension = os.path.splitext(filename)[1] 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 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 - # of BuildPlateDecorator produces one that's assoicated with build plate -1. So, here we need to check if + # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy + # 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. build_plate_decorator = node.getDecorator(BuildPlateDecorator) if build_plate_decorator is None: @@ -1508,6 +1533,8 @@ class CuraApplication(QtApplication): op.push() scene.sceneChanged.emit(node) + self.fileCompleted.emit(filename) + def addNonSliceableExtension(self, extension): self._non_sliceable_extensions.append(extension) diff --git a/cura/Scene/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py index c8106b5d15..6c8c201498 100644 --- a/cura/Scene/ConvexHullNode.py +++ b/cura/Scene/ConvexHullNode.py @@ -24,7 +24,10 @@ class ConvexHullNode(SceneNode): self._original_parent = parent # 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. self._mesh_height = 0.1 diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index 9df2931f0b..1635d37b5d 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -1,5 +1,6 @@ from UM.Application import Application from UM.Logger import Logger +from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Scene.SceneNode import SceneNode from copy import deepcopy @@ -9,7 +10,7 @@ from copy import deepcopy class CuraSceneNode(SceneNode): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._outside_buildarea = True + self._outside_buildarea = False def setOutsideBuildArea(self, new_value): self._outside_buildarea = new_value diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 06ccd300ab..309f27b6d7 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -764,7 +764,7 @@ class MachineManager(QObject): ## Set the active material by switching out a container # Depending on from/to material+current variant, a quality profile is chosen and set. @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): containers = ContainerRegistry.getInstance().findInstanceContainers(id = material_id) if not containers or not self._active_container_stack: @@ -846,10 +846,10 @@ class MachineManager(QObject): if not old_quality_changes: new_quality_id = candidate_quality.getId() - self.setActiveQuality(new_quality_id) + self.setActiveQuality(new_quality_id, always_discard_changes = always_discard_changes) @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): containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant_id) if not containers or not self._active_container_stack: @@ -865,7 +865,7 @@ class MachineManager(QObject): if old_material: 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 - self.setActiveMaterial(preferred_material_id) + self.setActiveMaterial(preferred_material_id, always_discard_changes = always_discard_changes) else: 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 # \param quality_id The quality_id of either a quality or a quality_changes @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): self.blurSettings.emit() + Logger.log("d", "Attempting to change the active quality to %s", quality_id) + containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = quality_id) if not containers or not self._global_container_stack: return @@ -948,11 +950,13 @@ class MachineManager(QObject): "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 # the dialog will be the those before the switching. 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() ## 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. # The Application.onDiscardOrKeepProfileChangesClosed signal triggers this method. def _executeDelayedActiveContainerStackChanges(self): + + Logger.log("d", "Applying configuration changes...") + if self._new_variant_container is not None: self._active_container_stack.variant = self._new_variant_container self._new_variant_container = None @@ -984,6 +991,8 @@ class MachineManager(QObject): self._new_quality_containers.clear() + Logger.log("d", "New configuration applied") + ## Cancel set changes for material and variant in the active container stack. # Used for ignoring any changes when switching between printers (setActiveMachine) def _cancelDelayedActiveContainerStackChanges(self): diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 85699ea0f5..06d38eccd0 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -291,6 +291,7 @@ class CuraEngineBackend(QObject, Backend): self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: + self.backendStateChange.emit(BackendState.Error) return if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: