mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-08 23:46:22 -06:00
Merge pull request #3031 from Ultimaker/feature_headless_docker
Feature headless
This commit is contained in:
commit
fb8aa08c6a
7 changed files with 138 additions and 79 deletions
|
@ -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)
|
||||
|
|
|
@ -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,19 +212,39 @@ 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
|
||||
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"):
|
||||
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
|
||||
node.setOutsideBuildArea(True)
|
||||
return
|
||||
|
||||
convex_hull = node.callDecoration("getConvexHull")
|
||||
convex_hull = self.callDecoration("getConvexHull")
|
||||
if convex_hull:
|
||||
if not convex_hull.isValid():
|
||||
return
|
||||
|
@ -233,13 +253,9 @@ class BuildVolume(SceneNode):
|
|||
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.
|
||||
for group_node in group_nodes:
|
||||
for child_node in group_node.getAllChildren():
|
||||
child_node._outside_buildarea = group_node._outside_buildarea
|
||||
node.setOutsideBuildArea(True)
|
||||
return
|
||||
node.setOutsideBuildArea(False)
|
||||
|
||||
## Recalculates the build volume & disallowed areas.
|
||||
def rebuild(self):
|
||||
|
|
|
@ -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))
|
||||
|
||||
run_without_gui = self.getCommandLineOption("headless", False)
|
||||
if not run_without_gui:
|
||||
self.initializeEngine()
|
||||
|
||||
# Make sure the correct stage is activated after QML is loaded
|
||||
controller.setActiveStage("PrepareStage")
|
||||
|
||||
if run_without_gui or self._engine.rootObjects:
|
||||
# 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)
|
||||
|
||||
|
|
|
@ -24,7 +24,10 @@ class ConvexHullNode(SceneNode):
|
|||
self._original_parent = parent
|
||||
|
||||
# Color of the drawn convex hull
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue