Browse Source

Merge pull request #3031 from Ultimaker/feature_headless_docker

Feature headless
ChrisTerBeke 7 years ago
parent
commit
fb8aa08c6a

+ 2 - 0
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)

+ 42 - 26
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:

+ 72 - 45
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")
-
-        if run_without_gui or self._engine.rootObjects:
-            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)
+        # Make sure the correct stage is activated after QML is loaded
+        controller.setActiveStage("PrepareStage")
 
-            self._started = True
+        # Hide the splash screen
+        self.closeSplash()
 
-            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)
 

+ 4 - 1
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

+ 2 - 1
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

+ 15 - 6
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):

+ 1 - 0
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: